Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ddc554a7ef | ||
|
|
b6d27b5333 | ||
|
|
2e54537524 | ||
|
|
0d8fa1a0ff | ||
|
|
764d0c1008 | ||
|
|
5f86d6158b | ||
|
|
1ec9d1d7bd | ||
|
|
462a9deef3 | ||
|
|
c9d6ea50db | ||
|
|
d0694b5d58 | ||
|
|
16fe53bc10 | ||
|
|
a9a271e360 | ||
|
|
62697dfa00 | ||
|
|
132b80acb7 | ||
|
|
f5e35dade5 | ||
|
|
3d72978af2 | ||
|
|
51d61cbdfb | ||
|
|
4d3084b3fb | ||
|
|
09a13b0600 | ||
|
|
5f96a9b37f | ||
|
|
762b5c0870 |
+25
-1
@@ -16,7 +16,7 @@ The Elixir codebase itself has been already fully formatted and all further cont
|
||||
|
||||
Supervisors in Elixir are responsible for starting, shutting down and restarting child process when things go wrong. Most of the interaction with supervisors happen with the Supervisor module and it contains three main strategies: `:one_for_one`, `:rest_for_one` and `:one_for_all`.
|
||||
|
||||
However, sometimes the children of a supervisor are not known upfront and are rather started dynamically. For example, if you are building a web server, you have each request beind handled by a separate supervised process. Those cases were handled in the Supervisor module under a special strategy called `:simple_one_for_one`.
|
||||
However, sometimes the children of a supervisor are not known upfront and are rather started dynamically. For example, if you are building a web server, you have each request being handled by a separate supervised process. Those cases were handled in the Supervisor module under a special strategy called `:simple_one_for_one`.
|
||||
|
||||
Unfortunately, this special strategy changed the semantics of the supervisor in regards to initialization and shutdown. Plus some APIs expected different inputs or would be completely unavailable depending on the supervision strategy.
|
||||
|
||||
@@ -123,12 +123,36 @@ Those improvements will help developers better understand the relationship betwe
|
||||
|
||||
Other improvements in Mix include better compiler diagnostics for editor integration, support for the `--slowest N` flag in `mix test` that shows the slowest tests in your suite, and a new `mix profile.eprof` task that provides time based profiling, complementing the existing `mix profile.cprof` (count based) and `mix profile.fprof` (flame based).
|
||||
|
||||
## v1.6.6 (2018-06-20)
|
||||
|
||||
This release supports Erlang/OTP 21.0 by removing all warnings and by properly supporting the new Erlang logger module.
|
||||
|
||||
### 1. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Base] Do not raise when finding bad digits in `Base.decode32!` with `case: :mixed`
|
||||
* [Code] Preserve the user's choice when `fn` is followed by a newline and it has only a single clause
|
||||
* [DynamicSupervisor] Properly account for restarting children in the `:max_children` configuration
|
||||
* [String] Add performant impl for string upcase/downcase `:ascii` mode
|
||||
* [Task.Supervisor] Fix type spec for `start_child/4`
|
||||
|
||||
#### Logger
|
||||
|
||||
* [Logger] Do not crash truncation when truncate is set to infinity
|
||||
|
||||
#### Mix
|
||||
|
||||
* [mix format] Match files starting with dot
|
||||
|
||||
## v1.6.5 (2018-05-07)
|
||||
|
||||
This release supports Erlang/OTP 21.0-rc by removing all warnings and by properly redirecting logger output. Note it is not guaranteed it will support Erlang/OTP 21.0 final.
|
||||
|
||||
### 1. Bug fixes
|
||||
|
||||
#### Elixir
|
||||
|
||||
* [Code] Preserve the user's choice in the formatter on parens call with next break fits
|
||||
* [Code] Preserve the user's choice in the formatter on calls without parens when we have one argument per line
|
||||
* [Code] Fix formatting when there is a tilde in the first element of a bitstring
|
||||
|
||||
+13
-10
@@ -184,16 +184,19 @@ defmodule Base do
|
||||
end
|
||||
|
||||
defp decode_char_clauses(alphabet, :mixed) when length(alphabet) == 32 do
|
||||
alphabet
|
||||
|> Stream.with_index()
|
||||
|> Enum.flat_map(fn {encoding, value} = pair ->
|
||||
if encoding in ?A..?Z do
|
||||
[pair, {encoding - ?A + ?a, value}]
|
||||
else
|
||||
[pair]
|
||||
end
|
||||
end)
|
||||
|> decode_clauses()
|
||||
clauses =
|
||||
alphabet
|
||||
|> Stream.with_index()
|
||||
|> Enum.flat_map(fn {encoding, value} = pair ->
|
||||
if encoding in ?A..?Z do
|
||||
[pair, {encoding - ?A + ?a, value}]
|
||||
else
|
||||
[pair]
|
||||
end
|
||||
end)
|
||||
|> decode_clauses()
|
||||
|
||||
clauses ++ bad_digit_clause()
|
||||
end
|
||||
|
||||
defp decode_mixed_clauses(first, second) do
|
||||
|
||||
@@ -522,7 +522,7 @@ defmodule Code.Formatter do
|
||||
end
|
||||
|
||||
defp quoted_to_algebra({:fn, meta, [_ | _] = clauses}, _context, state) do
|
||||
anon_fun_to_algebra(clauses, line(meta), end_line(meta), state)
|
||||
anon_fun_to_algebra(clauses, line(meta), end_line(meta), state, eol?(meta))
|
||||
end
|
||||
|
||||
defp quoted_to_algebra({fun, meta, args}, context, state) when is_atom(fun) and is_list(args) do
|
||||
@@ -1598,7 +1598,13 @@ defmodule Code.Formatter do
|
||||
## Anonymous functions
|
||||
|
||||
# fn -> block end
|
||||
defp anon_fun_to_algebra([{:->, meta, [[], body]}] = clauses, _min_line, max_line, state) do
|
||||
defp anon_fun_to_algebra(
|
||||
[{:->, meta, [[], body]}] = clauses,
|
||||
_min_line,
|
||||
max_line,
|
||||
state,
|
||||
_multi_clauses_style
|
||||
) do
|
||||
min_line = line(meta)
|
||||
{body_doc, state} = block_to_algebra(body, min_line, max_line, state)
|
||||
|
||||
@@ -1617,7 +1623,13 @@ defmodule Code.Formatter do
|
||||
# fn x ->
|
||||
# y
|
||||
# end
|
||||
defp anon_fun_to_algebra([{:->, meta, [args, body]}] = clauses, _min_line, max_line, state) do
|
||||
defp anon_fun_to_algebra(
|
||||
[{:->, meta, [args, body]}] = clauses,
|
||||
_min_line,
|
||||
max_line,
|
||||
state,
|
||||
false = _multi_clauses_style
|
||||
) do
|
||||
min_line = line(meta)
|
||||
{args_doc, state} = clause_args_to_algebra(args, min_line, state)
|
||||
{body_doc, state} = block_to_algebra(body, min_line, max_line, state)
|
||||
@@ -1647,7 +1659,7 @@ defmodule Code.Formatter do
|
||||
# args2 ->
|
||||
# block2
|
||||
# end
|
||||
defp anon_fun_to_algebra(clauses, min_line, max_line, state) do
|
||||
defp anon_fun_to_algebra(clauses, min_line, max_line, state, _multi_clauses_style) do
|
||||
{clauses_doc, state} = clauses_to_algebra(clauses, min_line, max_line, state)
|
||||
{"fn" |> line(clauses_doc) |> nest(2) |> line("end") |> force_unfit(), state}
|
||||
end
|
||||
@@ -2139,6 +2151,10 @@ defmodule Code.Formatter do
|
||||
false
|
||||
end
|
||||
|
||||
defp eol?(meta) do
|
||||
Keyword.get(meta, :eol, false)
|
||||
end
|
||||
|
||||
defp line(meta) do
|
||||
Keyword.get(meta, :line, @max_line)
|
||||
end
|
||||
|
||||
@@ -137,13 +137,14 @@ defmodule DynamicSupervisor do
|
||||
"""
|
||||
@callback init(args :: term) :: {:ok, sup_flags()} | :ignore
|
||||
|
||||
@opaque sup_flags() :: %{
|
||||
strategy: strategy(),
|
||||
intensity: non_neg_integer(),
|
||||
period: pos_integer(),
|
||||
max_children: non_neg_integer() | :infinity,
|
||||
extra_arguments: [term()]
|
||||
}
|
||||
@typedoc "The supervisor flags returned on init"
|
||||
@type sup_flags() :: %{
|
||||
strategy: strategy(),
|
||||
intensity: non_neg_integer(),
|
||||
period: pos_integer(),
|
||||
max_children: non_neg_integer() | :infinity,
|
||||
extra_arguments: [term()]
|
||||
}
|
||||
|
||||
@typedoc "Option values used by the `start*` functions"
|
||||
@type option :: {:name, Supervisor.name()} | init_option()
|
||||
@@ -179,7 +180,6 @@ defmodule DynamicSupervisor do
|
||||
:max_restarts,
|
||||
:max_seconds,
|
||||
children: %{},
|
||||
dynamic: 0,
|
||||
restarts: []
|
||||
]
|
||||
|
||||
@@ -409,7 +409,7 @@ defmodule DynamicSupervisor do
|
||||
|
||||
The map contains the following keys:
|
||||
|
||||
* `:specs` - always 1 as dynamic supervisors have a single specification
|
||||
* `:specs` - the number of children processes
|
||||
|
||||
* `:active` - the count of all actively running child processes managed by
|
||||
this supervisor
|
||||
@@ -625,10 +625,10 @@ defmodule DynamicSupervisor do
|
||||
end
|
||||
|
||||
def handle_call({:start_child, child}, _from, state) do
|
||||
%{dynamic: dynamic, max_children: max_children} = state
|
||||
%{children: children, max_children: max_children} = state
|
||||
|
||||
if dynamic < max_children do
|
||||
handle_start_child(child, %{state | dynamic: dynamic + 1})
|
||||
if map_size(children) < max_children do
|
||||
handle_start_child(child, state)
|
||||
else
|
||||
{:reply, {:error, :max_children}, state}
|
||||
end
|
||||
@@ -645,7 +645,7 @@ defmodule DynamicSupervisor do
|
||||
{:reply, reply, save_child(pid, mfa, restart, shutdown, type, modules, state)}
|
||||
|
||||
_ ->
|
||||
{:reply, reply, update_in(state.dynamic, &(&1 - 1))}
|
||||
{:reply, reply, state}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -664,14 +664,14 @@ defmodule DynamicSupervisor do
|
||||
end
|
||||
end
|
||||
|
||||
defp save_child(pid, {m, f, _}, :temporary, shutdown, type, modules, state) do
|
||||
put_in(state.children[pid], {{m, f, :undefined}, :temporary, shutdown, type, modules})
|
||||
end
|
||||
|
||||
defp save_child(pid, mfa, restart, shutdown, type, modules, state) do
|
||||
mfa = mfa_for_restart(mfa, restart)
|
||||
put_in(state.children[pid], {mfa, restart, shutdown, type, modules})
|
||||
end
|
||||
|
||||
defp mfa_for_restart({m, f, _}, :temporary), do: {m, f, :undefined}
|
||||
defp mfa_for_restart(mfa, _), do: mfa
|
||||
|
||||
defp exit_reason(:exit, reason, _), do: reason
|
||||
defp exit_reason(:error, reason, stack), do: {reason, stack}
|
||||
defp exit_reason(:throw, value, stack), do: {{:nocatch, value}, stack}
|
||||
@@ -888,9 +888,8 @@ defmodule DynamicSupervisor do
|
||||
{:ok, delete_child(pid, state)}
|
||||
end
|
||||
|
||||
defp delete_child(pid, state) do
|
||||
%{children: children, dynamic: dynamic} = state
|
||||
%{state | children: Map.delete(children, pid), dynamic: dynamic - 1}
|
||||
defp delete_child(pid, %{children: children} = state) do
|
||||
%{state | children: Map.delete(children, pid)}
|
||||
end
|
||||
|
||||
defp restart_child(pid, child, state) do
|
||||
|
||||
@@ -117,7 +117,7 @@ defmodule Port do
|
||||
reimplementing core part of the Runtime System, such as the `:user` and
|
||||
`:shell` processes.
|
||||
|
||||
## Zombie processes
|
||||
## Zombie OS processes
|
||||
|
||||
A port can be closed via the `close/1` function or by sending a `{pid, :close}`
|
||||
message. However, if the VM crashes, a long-running program started by the port
|
||||
|
||||
@@ -644,15 +644,19 @@ defmodule String do
|
||||
end
|
||||
|
||||
def upcase(string, :ascii) when is_binary(string) do
|
||||
for <<x <- string>>,
|
||||
do: if(x >= ?a and x <= ?z, do: <<x - 32>>, else: <<x>>),
|
||||
into: ""
|
||||
IO.iodata_to_binary(upcase_ascii(string))
|
||||
end
|
||||
|
||||
def upcase(string, mode) when mode in @conditional_mappings do
|
||||
String.Casing.upcase(string, [], mode)
|
||||
end
|
||||
|
||||
defp upcase_ascii(<<char, rest::bits>>) when char >= ?a and char <= ?z,
|
||||
do: [char - 32 | upcase_ascii(rest)]
|
||||
|
||||
defp upcase_ascii(<<char, rest::bits>>), do: [char | upcase_ascii(rest)]
|
||||
defp upcase_ascii(<<>>), do: []
|
||||
|
||||
@doc """
|
||||
Converts all characters in the given string to lowercase according to `mode`.
|
||||
|
||||
@@ -700,15 +704,19 @@ defmodule String do
|
||||
end
|
||||
|
||||
def downcase(string, :ascii) when is_binary(string) do
|
||||
for <<x <- string>>,
|
||||
do: if(x >= ?A and x <= ?Z, do: <<x + 32>>, else: <<x>>),
|
||||
into: ""
|
||||
IO.iodata_to_binary(downcase_ascii(string))
|
||||
end
|
||||
|
||||
def downcase(string, mode) when mode in @conditional_mappings do
|
||||
String.Casing.downcase(string, [], mode)
|
||||
end
|
||||
|
||||
defp downcase_ascii(<<char, rest::bits>>) when char >= ?A and char <= ?Z,
|
||||
do: [char + 32 | downcase_ascii(rest)]
|
||||
|
||||
defp downcase_ascii(<<char, rest::bits>>), do: [char | downcase_ascii(rest)]
|
||||
defp downcase_ascii(<<>>), do: []
|
||||
|
||||
@doc """
|
||||
Converts the first character in the given string to
|
||||
uppercase and the remainder to lowercase according to `mode`.
|
||||
|
||||
@@ -317,7 +317,7 @@ defmodule Task.Supervisor do
|
||||
or an integer indicating the timeout value, defaults to 5000 milliseconds.
|
||||
|
||||
"""
|
||||
@spec start_child(Supervisor.supervisor(), (() -> any)) :: {:ok, pid}
|
||||
@spec start_child(Supervisor.supervisor(), (() -> any)) :: DynamicSupervisor.on_start_child()
|
||||
def start_child(supervisor, fun, options \\ []) do
|
||||
restart = options[:restart]
|
||||
shutdown = options[:shutdown]
|
||||
@@ -331,7 +331,8 @@ defmodule Task.Supervisor do
|
||||
Similar to `start_child/2` except the task is specified
|
||||
by the given `module`, `fun` and `args`.
|
||||
"""
|
||||
@spec start_child(Supervisor.supervisor(), module, atom, [term]) :: {:ok, pid}
|
||||
@spec start_child(Supervisor.supervisor(), module, atom, [term]) ::
|
||||
DynamicSupervisor.on_start_child()
|
||||
def start_child(supervisor, module, fun, args, options \\ [])
|
||||
when is_atom(fun) and is_list(args) do
|
||||
restart = options[:restart]
|
||||
|
||||
@@ -12,7 +12,7 @@ Elixir version | Supported Erlang/OTP versions
|
||||
1.3 | 18 - 19
|
||||
1.4 | 18 - 19 (and OTP 20 from v1.4.5)
|
||||
1.5 | 18 - 20
|
||||
1.6 | 19 - 20 (and OTP 21 from v1.6.5)
|
||||
1.6 | 19 - 20 (and OTP 21 from v1.6.6)
|
||||
|
||||
While Elixir often adds compatibility to new Erlang versions on released branches, such as support for OTP 20 in v1.4.5, those releases usually contain the minimum changes for Elixir to run without errors. Only the next minor release, in this case v1.5.0, does effectively leverage the new features provided by the latest Erlang release.
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{application, elixir,
|
||||
[{description, "elixir"},
|
||||
{vsn, "1.6.5"},
|
||||
{vsn, "1.6.6"},
|
||||
{modules, [
|
||||
elixir
|
||||
]},
|
||||
|
||||
@@ -305,7 +305,7 @@ eoe -> ';' : '$1'.
|
||||
eoe -> eol ';' : '$1'.
|
||||
|
||||
fn_eoe -> 'fn' : '$1'.
|
||||
fn_eoe -> 'fn' eoe : '$1'.
|
||||
fn_eoe -> 'fn' eoe : next_is_eol('$1').
|
||||
|
||||
do_eoe -> 'do' : '$1'.
|
||||
do_eoe -> 'do' eoe : '$1'.
|
||||
@@ -659,7 +659,7 @@ meta_from_token_with_end_line(Begin, End) ->
|
||||
case ?formatter_metadata() of
|
||||
true ->
|
||||
[{end_line, line_from_location(?location(End))}
|
||||
| meta_from_token(Begin)];
|
||||
| eol_op(?location(Begin)) ++ meta_from_token(Begin)];
|
||||
false ->
|
||||
meta_from_token(Begin)
|
||||
end.
|
||||
|
||||
@@ -473,7 +473,7 @@ defmodule BaseTest do
|
||||
assert :error == decode32("66FF", case: :lower)
|
||||
end
|
||||
|
||||
test "decode32!/1,2 error on non-alphabet digit" do
|
||||
test "decode32!/1,2 argument error on non-alphabet digit" do
|
||||
assert_raise ArgumentError, "non-alphabet digit found: \")\" (byte 41)", fn ->
|
||||
decode32!("MZX)6YTB")
|
||||
end
|
||||
@@ -485,6 +485,10 @@ defmodule BaseTest do
|
||||
assert_raise ArgumentError, "non-alphabet digit found: \"M\" (byte 77)", fn ->
|
||||
decode32!("MZXW6YTBOI======", case: :lower)
|
||||
end
|
||||
|
||||
assert_raise ArgumentError, "non-alphabet digit found: \"0\" (byte 48)", fn ->
|
||||
decode32!("0ZXW6YTB0I======", case: :mixed)
|
||||
end
|
||||
end
|
||||
|
||||
test "decode32/1 errors on incorrect padding" do
|
||||
|
||||
@@ -449,14 +449,15 @@ defmodule Code.Formatter.CommentsTest do
|
||||
|
||||
assert_format bad, ~S"""
|
||||
# fn
|
||||
# before head
|
||||
# middle head
|
||||
# after head
|
||||
fn hello ->
|
||||
# before body
|
||||
# middle body
|
||||
world
|
||||
# after body
|
||||
fn
|
||||
# before head
|
||||
# middle head
|
||||
hello ->
|
||||
# after head
|
||||
# before body
|
||||
# middle body
|
||||
world
|
||||
# after body
|
||||
end
|
||||
"""
|
||||
end
|
||||
|
||||
@@ -187,6 +187,22 @@ defmodule Code.Formatter.GeneralTest do
|
||||
assert_same code, @short_length
|
||||
end
|
||||
|
||||
test "with a single clause, followed by a newline, and can fit in one line" do
|
||||
assert_same """
|
||||
fn
|
||||
hello -> world
|
||||
end
|
||||
"""
|
||||
end
|
||||
|
||||
test "with a single clause, followed by a newline, and can not fit in one line" do
|
||||
assert_same """
|
||||
SomeModule.long_function_name_that_approaches_max_columns(argument, acc, fn
|
||||
%SomeStruct{key: key}, acc -> more_code(key, acc)
|
||||
end)
|
||||
"""
|
||||
end
|
||||
|
||||
test "with multiple clauses" do
|
||||
code = """
|
||||
fn
|
||||
|
||||
@@ -340,7 +340,7 @@ defmodule DynamicSupervisorTest do
|
||||
assert %{workers: 4, active: 2} = DynamicSupervisor.count_children(pid)
|
||||
end
|
||||
|
||||
test "restarting children counted in max_children" do
|
||||
test "restarting on init children counted in max_children" do
|
||||
child = current_module_worker([:restart, :error], restart: :permanent)
|
||||
opts = [strategy: :one_for_one, max_children: 1, max_restarts: 100_000]
|
||||
{:ok, pid} = DynamicSupervisor.start_link(opts)
|
||||
@@ -353,7 +353,20 @@ defmodule DynamicSupervisorTest do
|
||||
assert {:error, :max_children} = DynamicSupervisor.start_child(pid, child)
|
||||
end
|
||||
|
||||
test "restarting a child with extra_args successfully restarts child" do
|
||||
test "restarting on exit children counted in max_children" do
|
||||
child = current_module_worker([:ok2], restart: :permanent)
|
||||
opts = [strategy: :one_for_one, max_children: 1, max_restarts: 100_000]
|
||||
{:ok, pid} = DynamicSupervisor.start_link(opts)
|
||||
|
||||
assert {:ok, child_pid} = DynamicSupervisor.start_child(pid, child)
|
||||
assert_kill(child_pid, :shutdown)
|
||||
assert %{workers: 1, active: 1} = DynamicSupervisor.count_children(pid)
|
||||
|
||||
child = current_module_worker([:ok2], restart: :permanent)
|
||||
assert {:error, :max_children} = DynamicSupervisor.start_child(pid, child)
|
||||
end
|
||||
|
||||
test "restarting a child with extra_arguments successfully restarts child" do
|
||||
parent = self()
|
||||
|
||||
fun = fn ->
|
||||
|
||||
@@ -70,7 +70,7 @@ defmodule ExUnit.CaptureLog do
|
||||
{:ok, string_io} = StringIO.open("")
|
||||
|
||||
try do
|
||||
_ = :gen_event.which_handlers(:error_logger)
|
||||
_ = Process.whereis(:error_logger) && :gen_event.which_handlers(:error_logger)
|
||||
:ok = add_capture(string_io, opts)
|
||||
ref = ExUnit.CaptureServer.log_capture_on(self())
|
||||
|
||||
|
||||
@@ -240,10 +240,15 @@ defmodule IEx.Helpers do
|
||||
|
||||
subl path/to/file:line
|
||||
|
||||
Custom editors are supported by using the __FILE__ and __LINE__
|
||||
notations. For example, vi/vim users can set `ELIXIR_EDITOR` to:
|
||||
It is important that you choose an editor command that does
|
||||
not block nor that attempts to run an editor directly in the
|
||||
terminal. Command-line based editors likely extra configuration
|
||||
so they open up the given file and line in a separate window.
|
||||
|
||||
ELIXIR_EDITOR="vi +__LINE__ __FILE__"
|
||||
Custom editors are supported by using the __FILE__ and __LINE__
|
||||
notations, for example:
|
||||
|
||||
ELIXIR_EDITOR="my_editor +__LINE__ __FILE__"
|
||||
|
||||
and Elixir will properly interpolate values.
|
||||
|
||||
@@ -251,8 +256,8 @@ defmodule IEx.Helpers do
|
||||
`ELIXIR_EDITOR` can be set "echo" if you prefer to display the
|
||||
location rather than opening it.
|
||||
|
||||
Keep in mind the location may not exist when opening
|
||||
precompiled source code, such as Elixir itself.
|
||||
Keep in mind the location may not exist when opening precompiled
|
||||
source code.
|
||||
|
||||
## Examples
|
||||
|
||||
|
||||
@@ -195,30 +195,38 @@ defmodule IEx.HelpersTest do
|
||||
~r/#{@elixir_erl}:\d+$/
|
||||
end
|
||||
|
||||
test "opens OTP lists module" do
|
||||
assert capture_iex("open(:lists)") |> maybe_trim_quotes() =~ ~r/#{@lists_erl}:\d+$/
|
||||
# Some installations remove the source file once Erlang is compiled. See #7348.
|
||||
if File.regular?(@lists_erl) do
|
||||
test "opens OTP lists module" do
|
||||
assert capture_iex("open(:lists)") |> maybe_trim_quotes() =~ ~r/#{@lists_erl}:\d+$/
|
||||
end
|
||||
|
||||
test "opens OTP lists module.function" do
|
||||
assert capture_iex("open(:lists.reverse)") |> maybe_trim_quotes() =~
|
||||
~r/#{@lists_erl}:\d+$/
|
||||
end
|
||||
|
||||
test "opens OTP lists module.function/arity" do
|
||||
assert capture_iex("open(:lists.reverse/1)") |> maybe_trim_quotes() =~
|
||||
~r/#{@lists_erl}:\d+$/
|
||||
end
|
||||
end
|
||||
|
||||
test "opens OTP lists module.function" do
|
||||
assert capture_iex("open(:lists.reverse)") |> maybe_trim_quotes() =~ ~r/#{@lists_erl}:\d+$/
|
||||
end
|
||||
# Some installations remove the source file once Erlang is compiled. See #7348.
|
||||
if File.regular?(@httpc_erl) do
|
||||
test "opens OTP httpc module" do
|
||||
assert capture_iex("open(:httpc)") |> maybe_trim_quotes() =~ ~r/#{@httpc_erl}:\d+$/
|
||||
end
|
||||
|
||||
test "opens OTP lists module.function/arity" do
|
||||
assert capture_iex("open(:lists.reverse/1)") |> maybe_trim_quotes() =~
|
||||
~r/#{@lists_erl}:\d+$/
|
||||
end
|
||||
test "opens OTP httpc module.function" do
|
||||
assert capture_iex("open(:httpc.request)") |> maybe_trim_quotes() =~
|
||||
~r/#{@httpc_erl}:\d+$/
|
||||
end
|
||||
|
||||
test "opens OTP httpc module" do
|
||||
assert capture_iex("open(:httpc)") |> maybe_trim_quotes() =~ ~r/#{@httpc_erl}:\d+$/
|
||||
end
|
||||
|
||||
test "opens OTP httpc module.function" do
|
||||
assert capture_iex("open(:httpc.request)") |> maybe_trim_quotes() =~ ~r/#{@httpc_erl}:\d+$/
|
||||
end
|
||||
|
||||
test "opens OTP httpc module.function/arity" do
|
||||
assert capture_iex("open(:httpc.request/1)") |> maybe_trim_quotes() =~
|
||||
~r/#{@httpc_erl}:\d+$/
|
||||
test "opens OTP httpc module.function/arity" do
|
||||
assert capture_iex("open(:httpc.request/1)") |> maybe_trim_quotes() =~
|
||||
~r/#{@httpc_erl}:\d+$/
|
||||
end
|
||||
end
|
||||
|
||||
test "errors OTP preloaded module" do
|
||||
|
||||
+43
-35
@@ -16,8 +16,10 @@ defmodule Logger do
|
||||
performant when required but also apply backpressure
|
||||
when under stress.
|
||||
|
||||
* Wraps OTP's [`:error_logger`](http://erlang.org/doc/man/error_logger.html)
|
||||
to prevent it from overflowing.
|
||||
* Plugs into Erlang's [`:logger`](http://erlang.org/doc/man/logger.html)
|
||||
(from Erlang/OTP 21) to convert terms to Elixir syntax or wraps
|
||||
Erlang's [`:error_logger`](http://erlang.org/doc/man/error_logger.html)
|
||||
in earlier Erlang/OTP versions to prevent it from overflowing.
|
||||
|
||||
Logging is useful for tracking when an event of interest happens in your
|
||||
system. For example, it may be helpful to log whenever a user is deleted.
|
||||
@@ -62,8 +64,8 @@ defmodule Logger do
|
||||
* Runtime configuration - can be set before the `:logger`
|
||||
application is started, but may be changed during runtime
|
||||
|
||||
* Error logger configuration - configuration for the
|
||||
wrapper around OTP's [`:error_logger`](http://erlang.org/doc/man/error_logger.html)
|
||||
* Erlang configuration - options that handle integration with
|
||||
Erlang's logging facilities
|
||||
|
||||
### Application configuration
|
||||
|
||||
@@ -152,19 +154,24 @@ defmodule Logger do
|
||||
### Error logger configuration
|
||||
|
||||
The following configuration applies to `Logger`'s wrapper around
|
||||
OTP's [`:error_logger`](http://erlang.org/doc/man/error_logger.html).
|
||||
All the configurations below must be set before the `:logger` application starts.
|
||||
Erlang's logging functionalities. All the configurations below must
|
||||
be set before the `:logger` application starts.
|
||||
|
||||
* `:handle_otp_reports` - redirects OTP reports to `Logger` so
|
||||
they are formatted in Elixir terms. This uninstalls OTP's
|
||||
logger that prints terms to terminal. Defaults to `true`.
|
||||
they are formatted in Elixir terms. This effectively disables
|
||||
Erlang standard logger. Defaults to `true`.
|
||||
|
||||
* `:handle_sasl_reports` - redirects supervisor, crash and
|
||||
progress reports to `Logger` so they are formatted in Elixir
|
||||
terms. Your application must guarantee `:sasl` is started before
|
||||
`:logger`. This means you may see some initial reports written
|
||||
in Erlang syntax until the Logger application kicks in and
|
||||
uninstalls SASL's logger in favor of its own. Defaults to `false`.
|
||||
in Erlang syntax until the Logger application kicks in.
|
||||
Defaults to `false`.
|
||||
|
||||
From Erlang/OTP 21, `:handle_sasl_reports` only has an effect if
|
||||
`:handle_otp_reports` is true.
|
||||
|
||||
The following configurations apply only for Erlang/OTP 20 and earlier:
|
||||
|
||||
* `:discard_threshold_for_error_logger` - if `:error_logger` has more than
|
||||
`discard_threshold` messages in its inbox, messages will be dropped
|
||||
@@ -176,19 +183,17 @@ defmodule Logger do
|
||||
350 (0.75 * threshold) entries and 50 (0.1 * theshold) messages will
|
||||
be processed before the threshold is checked once again.
|
||||
|
||||
For example, to configure `Logger` to redirect all
|
||||
[`:error_logger`](http://erlang.org/doc/man/error_logger.html) messages
|
||||
using a `config/config.exs` file:
|
||||
For example, to configure `Logger` to redirect all Erlang messages using a
|
||||
`config/config.exs` file:
|
||||
|
||||
config :logger,
|
||||
handle_otp_reports: true,
|
||||
handle_sasl_reports: true
|
||||
|
||||
Furthermore, `Logger` allows messages sent by OTP's `:error_logger`
|
||||
to be translated into an Elixir format via translators. Translators
|
||||
can be dynamically added at any time with the `add_translator/1`
|
||||
and `remove_translator/1` APIs. Check `Logger.Translator` for more
|
||||
information.
|
||||
Furthermore, `Logger` allows messages sent by Erlang to be translated
|
||||
into an Elixir format via translators. Translators can be added at any
|
||||
time with the `add_translator/1` and `remove_translator/1` APIs. Check
|
||||
`Logger.Translator` for more information.
|
||||
|
||||
## Backends
|
||||
|
||||
@@ -523,7 +528,7 @@ defmodule Logger do
|
||||
"""
|
||||
@spec flush :: :ok
|
||||
def flush do
|
||||
_ = :gen_event.which_handlers(:error_logger)
|
||||
_ = Process.whereis(:error_logger) && :gen_event.which_handlers(:error_logger)
|
||||
:gen_event.sync_notify(Logger, :flush)
|
||||
end
|
||||
|
||||
@@ -533,8 +538,7 @@ defmodule Logger do
|
||||
## Options
|
||||
|
||||
* `:flush` - when `true`, guarantees all messages currently sent
|
||||
to both Logger and OTP's [`:error_logger`](http://erlang.org/doc/man/error_logger.html)
|
||||
are processed before the backend is added
|
||||
to `Logger` are processed before the backend is added
|
||||
|
||||
"""
|
||||
@spec add_backend(atom, keyword) :: Supervisor.on_start_child()
|
||||
@@ -560,8 +564,7 @@ defmodule Logger do
|
||||
## Options
|
||||
|
||||
* `:flush` - when `true`, guarantees all messages currently sent
|
||||
to both Logger and OTP's [`:error_logger`](http://erlang.org/doc/man/error_logger.html)
|
||||
are processed before the backend is removed
|
||||
to `Logger` are processed before the backend is removed
|
||||
|
||||
"""
|
||||
@spec remove_backend(atom, keyword) :: :ok | {:error, term}
|
||||
@@ -610,14 +613,17 @@ defmodule Logger do
|
||||
when level in @levels and is_list(metadata) do
|
||||
case __metadata__() do
|
||||
{true, pdict} ->
|
||||
%{mode: mode, truncate: truncate, level: min_level, utc_log: utc_log?} =
|
||||
Logger.Config.__data__()
|
||||
%{
|
||||
mode: mode,
|
||||
truncate: truncate,
|
||||
level: min_level,
|
||||
utc_log: utc_log?
|
||||
} = Logger.Config.__data__()
|
||||
|
||||
if compare_levels(level, min_level) != :lt and mode != :discard do
|
||||
metadata = [pid: self()] ++ Keyword.merge(pdict, metadata)
|
||||
{message, metadata} = normalize_message(chardata_or_fun, metadata)
|
||||
with true <- compare_levels(level, min_level) != :lt and mode != :discard,
|
||||
metadata = [pid: self()] ++ Keyword.merge(pdict, metadata),
|
||||
{message, metadata} <- normalize_message(chardata_or_fun, metadata) do
|
||||
truncated = truncate(message, truncate)
|
||||
|
||||
tuple = {Logger, truncated, Logger.Utils.timestamp(utc_log?), metadata}
|
||||
|
||||
try do
|
||||
@@ -629,7 +635,7 @@ defmodule Logger do
|
||||
:exit, reason -> {:error, reason}
|
||||
end
|
||||
else
|
||||
:ok
|
||||
_ -> :ok
|
||||
end
|
||||
|
||||
{false, _} ->
|
||||
@@ -755,15 +761,17 @@ defmodule Logger do
|
||||
end
|
||||
|
||||
defp normalize_message(fun, metadata) when is_function(fun, 0) do
|
||||
normalize_message(fun.(), metadata)
|
||||
case fun.() do
|
||||
{message, fun_metadata} -> {message, Keyword.merge(metadata, fun_metadata)}
|
||||
:skip -> :skip
|
||||
message -> {message, metadata}
|
||||
end
|
||||
end
|
||||
|
||||
defp normalize_message({message, fun_metadata}, metadata) when is_list(fun_metadata) do
|
||||
{message, Keyword.merge(metadata, fun_metadata)}
|
||||
defp normalize_message(message, metadata) do
|
||||
{message, metadata}
|
||||
end
|
||||
|
||||
defp normalize_message(message, metadata), do: {message, metadata}
|
||||
|
||||
defp truncate(data, n) when is_list(data) or is_binary(data), do: Logger.Utils.truncate(data, n)
|
||||
defp truncate(data, n), do: Logger.Utils.truncate(to_string(data), n)
|
||||
|
||||
|
||||
@@ -7,8 +7,15 @@ defmodule Logger.App do
|
||||
def start(_type, _args) do
|
||||
otp_reports? = Application.get_env(:logger, :handle_otp_reports)
|
||||
sasl_reports? = Application.get_env(:logger, :handle_sasl_reports)
|
||||
threshold = Application.get_env(:logger, :discard_threshold_for_error_logger)
|
||||
error_handler = {:error_logger, Logger.ErrorHandler, {otp_reports?, sasl_reports?, threshold}}
|
||||
|
||||
otp_children =
|
||||
if otp_logger?() do
|
||||
[]
|
||||
else
|
||||
threshold = Application.get_env(:logger, :discard_threshold_for_error_logger)
|
||||
arg = {:error_logger, Logger.ErrorHandler, {otp_reports?, sasl_reports?, threshold}}
|
||||
[%{id: Logger.ErrorHandler, start: {Logger.Watcher, :start_link, [arg]}}]
|
||||
end
|
||||
|
||||
children = [
|
||||
%{
|
||||
@@ -17,19 +24,21 @@ defmodule Logger.App do
|
||||
modules: :dynamic
|
||||
},
|
||||
{Logger.Watcher, {Logger, Logger.Config, []}},
|
||||
{Logger.WatcherSupervisor, {Logger.Config, :handlers, []}},
|
||||
%{
|
||||
id: Logger.ErrorHandler,
|
||||
start: {Logger.Watcher, :start_link, [error_handler]}
|
||||
}
|
||||
{Logger.WatcherSupervisor, {Logger.Config, :handlers, []}}
|
||||
| otp_children
|
||||
]
|
||||
|
||||
config = Logger.Config.new()
|
||||
|
||||
case Supervisor.start_link(children, strategy: :rest_for_one, name: Logger.Supervisor) do
|
||||
{:ok, sup} ->
|
||||
if otp_reports? or sasl_reports? do
|
||||
delete_handlers(otp_reports?, sasl_reports?)
|
||||
if otp_logger?() do
|
||||
if otp_reports? do
|
||||
add_elixir_handler(sasl_reports?)
|
||||
delete_erlang_handler()
|
||||
end
|
||||
else
|
||||
delete_old_handlers(otp_reports?, sasl_reports?)
|
||||
end
|
||||
|
||||
{:ok, sup, config}
|
||||
@@ -47,6 +56,10 @@ defmodule Logger.App do
|
||||
|
||||
@doc false
|
||||
def stop(config) do
|
||||
if otp_logger?() do
|
||||
_ = :logger.remove_handler(Logger)
|
||||
end
|
||||
|
||||
add_handlers(Logger.Config.deleted_handlers())
|
||||
Logger.Config.delete(config)
|
||||
end
|
||||
@@ -73,20 +86,35 @@ defmodule Logger.App do
|
||||
end
|
||||
end
|
||||
|
||||
defp delete_handlers(otp_reports?, sasl_reports?) do
|
||||
# TODO: Remove conditional error_logger code once we require Erlang/OTP 21+.
|
||||
defp otp_logger? do
|
||||
is_pid(Process.whereis(:logger))
|
||||
end
|
||||
|
||||
defp add_elixir_handler(sasl_reports?) do
|
||||
config = %{level: :debug, sasl_reports?: sasl_reports?}
|
||||
:logger.add_handler(Logger, Logger.ErlangHandler, config)
|
||||
end
|
||||
|
||||
defp delete_erlang_handler() do
|
||||
with {:ok, %{module: module} = config} <- :logger.get_handler_config(:default),
|
||||
:ok <- :logger.remove_handler(:default) do
|
||||
%{level: level} = :logger.get_primary_config()
|
||||
:logger.update_primary_config(%{level: :debug})
|
||||
primary_config = {:primary, %{level: level}}
|
||||
handler_config = {:default, module, config}
|
||||
[] = Logger.Config.deleted_handlers([primary_config, handler_config])
|
||||
:ok
|
||||
else
|
||||
_ -> :ok
|
||||
end
|
||||
end
|
||||
|
||||
defp delete_old_handlers(otp_reports?, sasl_reports?) do
|
||||
deleted =
|
||||
if is_pid(Process.whereis(:logger)) and Code.ensure_loaded?(:logger) do
|
||||
with {:ok, {module, config}} <- :logger.get_handler_config(:logger_std_h),
|
||||
:ok <- :logger.remove_handler(:logger_std_h) do
|
||||
[{:logger_std_h, module, config}]
|
||||
else
|
||||
_ -> []
|
||||
end
|
||||
else
|
||||
for {tty, true} <- [error_logger_tty_h: otp_reports?, sasl_report_tty_h: sasl_reports?],
|
||||
:error_logger.delete_report_handler(tty) != {:error, :module_not_found},
|
||||
do: tty
|
||||
end
|
||||
for {tty, true} <- [error_logger_tty_h: otp_reports?, sasl_report_tty_h: sasl_reports?],
|
||||
:error_logger.delete_report_handler(tty) != {:error, :module_not_found},
|
||||
do: tty
|
||||
|
||||
[] = Logger.Config.deleted_handlers(deleted)
|
||||
:ok
|
||||
@@ -95,8 +123,14 @@ defmodule Logger.App do
|
||||
defp add_handlers(handlers) do
|
||||
for handler <- handlers do
|
||||
case handler do
|
||||
{handler, module, config} -> :logger.add_handler(handler, module, config)
|
||||
handler -> :error_logger.add_report_handler(handler)
|
||||
{handler, module, config} ->
|
||||
:logger.add_handler(handler, module, config)
|
||||
|
||||
{:primary, config} ->
|
||||
:logger.update_primary_config(config)
|
||||
|
||||
handler ->
|
||||
:error_logger.add_report_handler(handler)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
defmodule Logger.ErlangHandler do
|
||||
@moduledoc false
|
||||
|
||||
@doc """
|
||||
Hook required by `:logger`.
|
||||
"""
|
||||
def log(%{meta: %{domain: [:otp, :sasl | _]}}, %{sasl_reports?: false}) do
|
||||
:ok
|
||||
end
|
||||
|
||||
def log(%{meta: %{domain: [:supervisor_report]}}, %{sasl_reports?: false}) do
|
||||
:ok
|
||||
end
|
||||
|
||||
def log(%{level: level, msg: msg, meta: erl_meta}, _config) do
|
||||
level = erlang_level_to_elixir_level(level)
|
||||
|
||||
Logger.bare_log(level, fn ->
|
||||
try do
|
||||
meta = extract_metadata(erl_meta)
|
||||
|
||||
case msg do
|
||||
{:string, string} ->
|
||||
{string, meta}
|
||||
|
||||
{:report, %{label: label, report: report} = complete} when map_size(complete) == 2 ->
|
||||
translate(level, :report, {label, report}, meta, erl_meta)
|
||||
|
||||
{:report, %{label: {:error_logger, _}, format: format, args: args}} ->
|
||||
translate(level, :format, {format, args}, meta, erl_meta)
|
||||
|
||||
{:report, report} ->
|
||||
translate(level, :report, {:logger, report}, meta, erl_meta)
|
||||
|
||||
{format, args} ->
|
||||
translate(level, :format, {format, args}, meta, erl_meta)
|
||||
end
|
||||
rescue
|
||||
e ->
|
||||
[
|
||||
"Failure while translating Erlang's logger event\n",
|
||||
Exception.format(:error, e, System.stacktrace())
|
||||
]
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
defp erlang_level_to_elixir_level(:emergency), do: :error
|
||||
defp erlang_level_to_elixir_level(:alert), do: :error
|
||||
defp erlang_level_to_elixir_level(:critical), do: :error
|
||||
defp erlang_level_to_elixir_level(:error), do: :error
|
||||
defp erlang_level_to_elixir_level(:warning), do: :warn
|
||||
defp erlang_level_to_elixir_level(:notice), do: :info
|
||||
defp erlang_level_to_elixir_level(:info), do: :info
|
||||
defp erlang_level_to_elixir_level(:debug), do: :debug
|
||||
|
||||
defp extract_metadata(map) do
|
||||
metadata = []
|
||||
|
||||
metadata =
|
||||
case map do
|
||||
%{mfa: {mod, fun, arity}} -> [module: mod, function: form_fa(fun, arity)] ++ metadata
|
||||
_ -> metadata
|
||||
end
|
||||
|
||||
metadata =
|
||||
case map do
|
||||
%{file: file, line: line} -> [file: List.to_string(file), line: line] ++ metadata
|
||||
_ -> metadata
|
||||
end
|
||||
|
||||
metadata =
|
||||
case map do
|
||||
%{pid: pid} -> [pid: pid] ++ metadata
|
||||
_ -> metadata
|
||||
end
|
||||
|
||||
metadata
|
||||
rescue
|
||||
_ -> []
|
||||
end
|
||||
|
||||
defp form_fa(fun, arity) do
|
||||
Atom.to_string(fun) <> "/" <> Integer.to_string(arity)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Shared translation convenience.
|
||||
"""
|
||||
def translate(level, kind, data, meta, erl_meta) do
|
||||
%{
|
||||
level: min_level,
|
||||
truncate: truncate,
|
||||
translators: translators
|
||||
} = Logger.Config.__data__()
|
||||
|
||||
case translate(translators, min_level, level, kind, data, meta) do
|
||||
:none -> {translate_fallback(kind, data, erl_meta, truncate), meta}
|
||||
other -> other
|
||||
end
|
||||
end
|
||||
|
||||
defp translate([{mod, fun} | t], min_level, level, kind, data, meta) do
|
||||
case apply(mod, fun, [min_level, level, kind, data]) do
|
||||
{:ok, chardata, transdata} -> {chardata, Keyword.merge(meta, transdata)}
|
||||
{:ok, chardata} -> {chardata, meta}
|
||||
:skip -> :skip
|
||||
:none -> translate(t, min_level, level, kind, data, meta)
|
||||
end
|
||||
end
|
||||
|
||||
defp translate([], _min_level, _level, _kind, _data, _meta) do
|
||||
:none
|
||||
end
|
||||
|
||||
defp translate_fallback(:report, {:logger, data}, %{report_cb: callback} = meta, truncate) do
|
||||
translate_fallback(:format, callback.(data), meta, truncate)
|
||||
end
|
||||
|
||||
defp translate_fallback(:format, {format, args}, _meta, truncate) do
|
||||
format
|
||||
|> Logger.Utils.scan_inspect(args, truncate)
|
||||
|> :io_lib.build_text()
|
||||
end
|
||||
|
||||
defp translate_fallback(:report, {_type, %{} = data}, _meta, _truncate) do
|
||||
Kernel.inspect(Map.to_list(data))
|
||||
end
|
||||
|
||||
defp translate_fallback(:report, {_type, data}, _meta, _truncate) do
|
||||
Kernel.inspect(data)
|
||||
end
|
||||
end
|
||||
@@ -2,6 +2,8 @@ defmodule Logger.ErrorHandler do
|
||||
@moduledoc false
|
||||
@behaviour :gen_event
|
||||
|
||||
# TODO: Remove this when we require Erlang/OTP 21+.
|
||||
|
||||
def init({otp?, sasl?, threshold}) do
|
||||
# We store the Logger PID in the state because when we are shutting
|
||||
# down the Logger application, the Logger process may be terminated
|
||||
@@ -83,17 +85,13 @@ defmodule Logger.ErrorHandler do
|
||||
%{
|
||||
mode: mode,
|
||||
level: min_level,
|
||||
truncate: truncate,
|
||||
utc_log: utc_log?,
|
||||
translators: translators
|
||||
utc_log: utc_log?
|
||||
} = Logger.Config.__data__()
|
||||
|
||||
with true <- Logger.compare_levels(level, min_level) != :lt and mode != :discard,
|
||||
{:ok, message} <- translate(translators, min_level, level, kind, data, truncate) do
|
||||
message = Logger.Utils.truncate(message, truncate)
|
||||
|
||||
meta = [pid: ensure_pid(pid), error_logger: ensure_type(type)],
|
||||
{message, meta} <- Logger.ErlangHandler.translate(level, kind, data, meta, %{}) do
|
||||
# Mode is always async to avoid clogging the error_logger
|
||||
meta = [pid: ensure_pid(pid), error_logger: ensure_type(type)]
|
||||
event = {Logger, message, Logger.Utils.timestamp(utc_log?), meta}
|
||||
:gen_event.notify(state.logger, {level, gl, event})
|
||||
end
|
||||
@@ -155,25 +153,4 @@ defmodule Logger.ErrorHandler do
|
||||
0 -> :ok
|
||||
end
|
||||
end
|
||||
|
||||
defp translate([{mod, fun} | t], min_level, level, kind, data, truncate) do
|
||||
case apply(mod, fun, [min_level, level, kind, data]) do
|
||||
{:ok, chardata} -> {:ok, chardata}
|
||||
:skip -> :skip
|
||||
:none -> translate(t, min_level, level, kind, data, truncate)
|
||||
end
|
||||
end
|
||||
|
||||
defp translate([], _min_level, _level, :format, {format, args}, truncate) do
|
||||
msg =
|
||||
format
|
||||
|> Logger.Utils.scan_inspect(args, truncate)
|
||||
|> :io_lib.build_text()
|
||||
|
||||
{:ok, msg}
|
||||
end
|
||||
|
||||
defp translate([], _min_level, _level, :report, {_type, data}, _truncate) do
|
||||
{:ok, Kernel.inspect(data)}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -20,7 +20,7 @@ defmodule Logger.Formatter do
|
||||
* `$message` - the log message
|
||||
* `$level` - the log level
|
||||
* `$node` - the node that prints the message
|
||||
* `$metadata` - user controlled data presented in `"key=val key2=val2"` format
|
||||
* `$metadata` - user controlled data presented in `"key=val key2=val2 "` format
|
||||
* `$levelpad` - sets to a single space if level is 4 characters long,
|
||||
otherwise set to the empty space. Used to align the message after level.
|
||||
|
||||
|
||||
+260
-130
@@ -3,7 +3,7 @@ defmodule Logger.Translator do
|
||||
Default translation for Erlang log messages.
|
||||
|
||||
Logger allows developers to rewrite log messages provided by
|
||||
Erlang applications into a format more compatible with Elixir
|
||||
OTP applications into a format more compatible with Elixir
|
||||
log messages by providing a translator.
|
||||
|
||||
A translator is simply a tuple containing a module and a function
|
||||
@@ -13,14 +13,15 @@ defmodule Logger.Translator do
|
||||
|
||||
* `min_level` - the current Logger level
|
||||
* `level` - the level of the message being translated
|
||||
* `kind` - if the message is a report or a format
|
||||
* `message` - the message to format. If it is a report, it is a tuple
|
||||
with `{report_type, report_data}`, if it is a format, it is a
|
||||
tuple with `{format_message, format_args}`
|
||||
* `kind` - if the message is a `:report` or `:format`
|
||||
* `message` - the message to format. If it is `:report`, it is a tuple
|
||||
with `{report_type, report_data}`, if it is `:format`, it is a
|
||||
tuple with `{format_message, format_args}`.
|
||||
|
||||
The function must return:
|
||||
|
||||
* `{:ok, chardata}` - if the message was translated with its translation
|
||||
* `{:ok, chardata, metadata}` - if the message translation with its metadata
|
||||
* `{:ok, chardata}` - the translated message
|
||||
* `:skip` - if the message is not meant to be translated nor logged
|
||||
* `:none` - if there is no translation, which triggers the next translator
|
||||
|
||||
@@ -28,17 +29,67 @@ defmodule Logger.Translator do
|
||||
and the default messages translated by Logger.
|
||||
"""
|
||||
|
||||
# The name_or_id checks are required to support old OTP projects.
|
||||
|
||||
@doc """
|
||||
Built-in translation function.
|
||||
"""
|
||||
def translate(min_level, level, kind, message)
|
||||
|
||||
## Erlang/OTP 21 and after
|
||||
|
||||
def translate(min_level, _level, :report, {:logger, %{label: label} = report}) do
|
||||
case label do
|
||||
{:gen_server, :terminate} ->
|
||||
report_gen_server_terminate(min_level, report)
|
||||
|
||||
{:gen_event, :terminate} ->
|
||||
report_gen_event_terminate(min_level, report)
|
||||
|
||||
_ ->
|
||||
:skip
|
||||
end
|
||||
end
|
||||
|
||||
def translate(min_level, _level, :report, {{:proc_lib, :crash}, data}) do
|
||||
report_crash(min_level, data)
|
||||
end
|
||||
|
||||
def translate(min_level, _level, :report, {{:supervisor, :progress}, data}) do
|
||||
report_supervisor_progress(min_level, data)
|
||||
end
|
||||
|
||||
def translate(min_level, _level, :report, {{:supervisor, _}, data}) do
|
||||
report_supervisor(min_level, data)
|
||||
end
|
||||
|
||||
def translate(
|
||||
_min_level,
|
||||
_level,
|
||||
:report,
|
||||
{{:application_controller, :progress}, [application: app, started_at: node]}
|
||||
) do
|
||||
{:ok, ["Application ", Atom.to_string(app), " started at " | inspect(node)]}
|
||||
end
|
||||
|
||||
def translate(
|
||||
_min_level,
|
||||
_level,
|
||||
:report,
|
||||
{{:application_controller, :exit}, [application: app, exited: reason, type: _type]}
|
||||
) do
|
||||
{:ok, ["Application ", Atom.to_string(app), " exited: " | Application.format_error(reason)]}
|
||||
end
|
||||
|
||||
## Erlang/OTP 20 and before
|
||||
|
||||
def translate(min_level, :error, :format, message) do
|
||||
opts = Application.get_env(:logger, :translator_inspect_opts)
|
||||
|
||||
case message do
|
||||
{'** Generic server ' ++ _, [name, last, state, reason | client]} ->
|
||||
{formatted, _reason} = format_reason(reason)
|
||||
|
||||
msg =
|
||||
["GenServer #{inspect(name)} terminating", format_stop(reason)] ++
|
||||
["GenServer #{inspect(name)} terminating", formatted] ++
|
||||
["\nLast message#{format_from(client)}: #{inspect(last, opts)}"]
|
||||
|
||||
if min_level == :debug do
|
||||
@@ -48,9 +99,11 @@ defmodule Logger.Translator do
|
||||
end
|
||||
|
||||
{'** gen_event handler ' ++ _, [name, manager, last, state, reason]} ->
|
||||
{formatted, _reason} = format_reason(reason)
|
||||
|
||||
msg =
|
||||
["GenEvent handler #{inspect(name)} installed in #{inspect(manager)} terminating"] ++
|
||||
[format_stop(reason), "\nLast message: #{inspect(last, opts)}"]
|
||||
[formatted, "\nLast message: #{inspect(last, opts)}"]
|
||||
|
||||
if min_level == :debug do
|
||||
{:ok, [msg | "\nState: #{inspect(state, opts)}"]}
|
||||
@@ -59,9 +112,11 @@ defmodule Logger.Translator do
|
||||
end
|
||||
|
||||
{'** Task ' ++ _, [name, starter, function, args, reason]} ->
|
||||
{formatted, _reason} = format_reason(reason)
|
||||
|
||||
msg =
|
||||
["Task #{inspect(name)} started from #{inspect(starter)} terminating"] ++
|
||||
[format_stop(reason), "\nFunction: #{inspect(function, opts)}"] ++
|
||||
[formatted, "\nFunction: #{inspect(function, opts)}"] ++
|
||||
["\n Args: #{inspect(args, opts)}"]
|
||||
|
||||
{:ok, msg}
|
||||
@@ -80,109 +135,102 @@ defmodule Logger.Translator do
|
||||
:std_info,
|
||||
[application: app, exited: reason, type: _type]
|
||||
}) do
|
||||
{:ok, "Application #{app} exited: #{Application.format_error(reason)}"}
|
||||
{:ok, ["Application ", Atom.to_string(app), " exited: " | Application.format_error(reason)]}
|
||||
end
|
||||
|
||||
def translate(min_level, :error, :report, {{:error_logger, :error_report}, data}) do
|
||||
report_supervisor(min_level, data)
|
||||
end
|
||||
|
||||
def translate(min_level, :error, :report, {:supervisor_report, data}) do
|
||||
translate_supervisor(min_level, data)
|
||||
report_supervisor(min_level, data)
|
||||
end
|
||||
|
||||
def translate(min_level, :error, :report, {:crash_report, data}) do
|
||||
translate_crash(min_level, data)
|
||||
report_crash(min_level, data)
|
||||
end
|
||||
|
||||
def translate(min_level, :info, :report, {:progress, data}) do
|
||||
translate_progress(min_level, data)
|
||||
def translate(min_level, :info, :report, {:progress, [{:supervisor, _} | _] = data}) do
|
||||
report_supervisor_progress(min_level, data)
|
||||
end
|
||||
|
||||
def translate(_min_level, :info, :report, {:progress, [application: app, started_at: node]}) do
|
||||
{:ok, ["Application ", Atom.to_string(app), " started at " | inspect(node)]}
|
||||
end
|
||||
|
||||
## Helpers
|
||||
|
||||
def translate(_min_level, _level, _kind, _message) do
|
||||
:none
|
||||
end
|
||||
|
||||
defp translate_supervisor(
|
||||
min_level,
|
||||
supervisor: sup,
|
||||
errorContext: context,
|
||||
reason: reason,
|
||||
offender: [{:pid, pid}, {name_or_id, name} | offender]
|
||||
)
|
||||
when is_pid(pid) and context !== :shutdown and name_or_id in [:name, :id] do
|
||||
msg =
|
||||
["Child ", inspect(name), " of Supervisor ", sup_name(sup)] ++
|
||||
[?\s, sup_context(context), "\n** (exit) "] ++
|
||||
[offender_reason(reason, context), "\nPid: ", inspect(pid)] ++
|
||||
child_info(min_level, offender)
|
||||
defp report_gen_server_terminate(min_level, report) do
|
||||
inspect_opts = Application.get_env(:logger, :translator_inspect_opts)
|
||||
|
||||
{:ok, msg}
|
||||
%{
|
||||
client_info: client,
|
||||
last_message: last,
|
||||
name: name,
|
||||
reason: reason,
|
||||
state: state
|
||||
} = report
|
||||
|
||||
{formatted, _reason} = format_reason(reason)
|
||||
|
||||
msg =
|
||||
["GenServer ", inspect(name), " terminating", formatted] ++
|
||||
["\nLast message", format_last_message_from(client), ": ", inspect(last, inspect_opts)]
|
||||
|
||||
if min_level == :debug do
|
||||
{:ok, [msg, "\nState: ", inspect(state, inspect_opts) | format_client_info(client)]}
|
||||
else
|
||||
{:ok, msg}
|
||||
end
|
||||
end
|
||||
|
||||
defp translate_supervisor(
|
||||
min_level,
|
||||
supervisor: sup,
|
||||
errorContext: context,
|
||||
reason: reason,
|
||||
offender: [{:pid, _pid}, {name_or_id, name} | offender]
|
||||
)
|
||||
when name_or_id in [:name, :id] do
|
||||
msg =
|
||||
["Child ", inspect(name), " of Supervisor ", sup_name(sup)] ++
|
||||
[?\s, sup_context(context), "\n** (exit) ", offender_reason(reason, context)] ++
|
||||
child_info(min_level, offender)
|
||||
defp report_gen_event_terminate(min_level, report) do
|
||||
inspect_opts = Application.get_env(:logger, :translator_inspect_opts)
|
||||
|
||||
{:ok, msg}
|
||||
%{
|
||||
handler: handler,
|
||||
last_message: last,
|
||||
name: name,
|
||||
reason: reason,
|
||||
state: state
|
||||
} = report
|
||||
|
||||
reason =
|
||||
case reason do
|
||||
{:EXIT, why} -> why
|
||||
_ -> reason
|
||||
end
|
||||
|
||||
{formatted, _reason} = format_reason(reason)
|
||||
|
||||
msg =
|
||||
["GenEvent handler ", inspect(handler), " installed in ", inspect(name), " terminating"] ++
|
||||
[formatted, "\nLast message: ", inspect(last, inspect_opts)]
|
||||
|
||||
if min_level == :debug do
|
||||
{:ok, [msg, "\nState: ", inspect(state, inspect_opts)]}
|
||||
else
|
||||
{:ok, msg}
|
||||
end
|
||||
end
|
||||
|
||||
defp translate_supervisor(
|
||||
defp report_supervisor_progress(
|
||||
min_level,
|
||||
supervisor: sup,
|
||||
errorContext: context,
|
||||
reason: reason,
|
||||
offender: [{:pid, pid} | offender]
|
||||
started: [{:pid, pid}, {:id, id} | started]
|
||||
) do
|
||||
msg =
|
||||
["Child of Supervisor ", sup_name(sup), ?\s, sup_context(context), "\n** (exit) "] ++
|
||||
[offender_reason(reason, context), "\nPid: ", inspect(pid)] ++
|
||||
child_info(min_level, offender)
|
||||
["Child ", inspect(id), " of Supervisor ", sup_name(sup), " started"] ++
|
||||
["\nPid: ", inspect(pid)] ++ child_info(min_level, started)
|
||||
|
||||
{:ok, msg}
|
||||
end
|
||||
|
||||
defp translate_supervisor(
|
||||
min_level,
|
||||
supervisor: sup,
|
||||
errorContext: context,
|
||||
reason: reason,
|
||||
offender: [{:nb_children, n}, {name_or_id, name} | offender]
|
||||
)
|
||||
when name_or_id in [:name, :id] do
|
||||
msg =
|
||||
["Children ", inspect(name), " of Supervisor ", sup_name(sup), ?\s, sup_context(context)] ++
|
||||
["\n** (exit) ", offender_reason(reason, context), "\nNumber: ", inspect(n)] ++
|
||||
child_info(min_level, offender)
|
||||
|
||||
{:ok, msg}
|
||||
end
|
||||
|
||||
defp translate_supervisor(_min_level, _other), do: :none
|
||||
|
||||
defp translate_progress(_min_level, application: app, started_at: node_name) do
|
||||
{:ok, ["Application ", to_string(app), " started at " | inspect(node_name)]}
|
||||
end
|
||||
|
||||
defp translate_progress(
|
||||
min_level,
|
||||
supervisor: sup,
|
||||
started: [{:pid, pid}, {name_or_id, name} | started]
|
||||
)
|
||||
when name_or_id in [:name, :id] do
|
||||
msg =
|
||||
["Child ", inspect(name), " of Supervisor ", sup_name(sup)] ++
|
||||
[" started", "\nPid: ", inspect(pid)] ++ child_info(min_level, started)
|
||||
|
||||
{:ok, msg}
|
||||
end
|
||||
|
||||
defp translate_progress(
|
||||
defp report_supervisor_progress(
|
||||
min_level,
|
||||
supervisor: sup,
|
||||
started: [{:pid, pid} | started]
|
||||
@@ -194,7 +242,70 @@ defmodule Logger.Translator do
|
||||
{:ok, msg}
|
||||
end
|
||||
|
||||
defp translate_progress(_min_level, _other), do: :none
|
||||
defp report_supervisor_progress(_min_level, _other), do: :none
|
||||
|
||||
defp report_supervisor(
|
||||
min_level,
|
||||
supervisor: sup,
|
||||
errorContext: context,
|
||||
reason: reason,
|
||||
offender: [{:pid, pid}, {:id, id} | offender]
|
||||
) do
|
||||
pid_info =
|
||||
if is_pid(pid) and context != :shutdown do
|
||||
["\nPid: ", inspect(pid)]
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
msg =
|
||||
["Child ", inspect(id), " of Supervisor ", sup_name(sup)] ++
|
||||
[?\s, sup_context(context), "\n** (exit) ", offender_reason(reason, context)] ++
|
||||
pid_info ++ child_info(min_level, offender)
|
||||
|
||||
{:ok, msg}
|
||||
end
|
||||
|
||||
defp report_supervisor(
|
||||
min_level,
|
||||
supervisor: sup,
|
||||
errorContext: context,
|
||||
reason: reason,
|
||||
offender: [{:nb_children, n}, {:id, id} | offender]
|
||||
) do
|
||||
msg =
|
||||
["Children ", inspect(id), " of Supervisor ", sup_name(sup), ?\s, sup_context(context)] ++
|
||||
["\n** (exit) ", offender_reason(reason, context), "\nNumber: ", Integer.to_string(n)] ++
|
||||
child_info(min_level, offender)
|
||||
|
||||
{:ok, msg}
|
||||
end
|
||||
|
||||
defp report_supervisor(
|
||||
min_level,
|
||||
supervisor: sup,
|
||||
errorContext: context,
|
||||
reason: reason,
|
||||
offender: [{:pid, pid} | offender]
|
||||
) do
|
||||
msg =
|
||||
["Child of Supervisor ", sup_name(sup), ?\s, sup_context(context)] ++
|
||||
["\n** (exit) ", offender_reason(reason, context), "\nPid: ", inspect(pid)] ++
|
||||
child_info(min_level, offender)
|
||||
|
||||
{:ok, msg}
|
||||
end
|
||||
|
||||
defp report_supervisor(_min_level, _other), do: :none
|
||||
|
||||
# If start call raises reason will be of form {:EXIT, reason}
|
||||
defp offender_reason({:EXIT, reason}, :start_error) do
|
||||
Exception.format_exit(reason)
|
||||
end
|
||||
|
||||
defp offender_reason(reason, _context) do
|
||||
Exception.format_exit(reason)
|
||||
end
|
||||
|
||||
defp sup_name({:local, name}), do: inspect(name)
|
||||
defp sup_name({:global, name}), do: inspect(name)
|
||||
@@ -210,14 +321,20 @@ defmodule Logger.Translator do
|
||||
["\nStart Call: ", format_mfa(mod, fun, args) | child_debug(min_level, debug)]
|
||||
end
|
||||
|
||||
# Comes from bridge with MFA
|
||||
defp child_info(min_level, [{:mfa, {mod, fun, args}} | debug]) do
|
||||
["\nStart Call: ", format_mfa(mod, fun, args) | child_debug(min_level, debug)]
|
||||
end
|
||||
|
||||
# Comes from bridge with Mod
|
||||
defp child_info(min_level, [{:mod, mod} | debug]) do
|
||||
["\nStart Module: ", inspect(mod) | child_debug(min_level, debug)]
|
||||
end
|
||||
|
||||
defp child_info(_min_level, _child) do
|
||||
[]
|
||||
end
|
||||
|
||||
defp child_debug(:debug, restart_type: restart, shutdown: shutdown, child_type: type) do
|
||||
["\nRestart: ", inspect(restart), "\nShutdown: ", inspect(shutdown)] ++
|
||||
["\nType: ", inspect(type)]
|
||||
@@ -227,16 +344,7 @@ defmodule Logger.Translator do
|
||||
[]
|
||||
end
|
||||
|
||||
# If start call raises reason will be of form {:EXIT, reason}
|
||||
defp offender_reason({:EXIT, reason}, :start_error) do
|
||||
Exception.format_exit(reason)
|
||||
end
|
||||
|
||||
defp offender_reason(reason, _context) do
|
||||
Exception.format_exit(reason)
|
||||
end
|
||||
|
||||
defp translate_crash(min_level, [
|
||||
defp report_crash(min_level, [
|
||||
[
|
||||
{:initial_call, _} = initial_call,
|
||||
{:pid, pid},
|
||||
@@ -245,14 +353,16 @@ defmodule Logger.Translator do
|
||||
],
|
||||
linked
|
||||
]) do
|
||||
reason = Exception.normalize(kind, exception, stack)
|
||||
|
||||
msg =
|
||||
["Process ", crash_name(pid, name), " terminating", format(kind, exception, stack)] ++
|
||||
["Process ", crash_name(pid, name), " terminating", format(kind, reason, stack)] ++
|
||||
[crash_info(min_level, [initial_call | crashed])] ++ crash_linked(min_level, linked)
|
||||
|
||||
{:ok, msg}
|
||||
end
|
||||
|
||||
defp translate_crash(min_level, [
|
||||
defp report_crash(min_level, [
|
||||
[
|
||||
{:pid, pid},
|
||||
{:registered_name, name},
|
||||
@@ -260,8 +370,10 @@ defmodule Logger.Translator do
|
||||
],
|
||||
linked
|
||||
]) do
|
||||
reason = Exception.normalize(kind, exception, stack)
|
||||
|
||||
msg =
|
||||
["Process ", crash_name(pid, name), " terminating", format(kind, exception, stack)] ++
|
||||
["Process ", crash_name(pid, name), " terminating", format(kind, reason, stack)] ++
|
||||
[crash_info(min_level, crashed), crash_linked(min_level, linked)]
|
||||
|
||||
{:ok, msg}
|
||||
@@ -309,7 +421,6 @@ defmodule Logger.Translator do
|
||||
defp crash_debug(:current_stacktrace, stack, prefix) do
|
||||
stack_prefix = [prefix | " "]
|
||||
stacktrace = Enum.map(stack, &[stack_prefix | Exception.format_stacktrace_entry(&1)])
|
||||
|
||||
[prefix, "Current Stacktrace:" | stacktrace]
|
||||
end
|
||||
|
||||
@@ -339,43 +450,59 @@ defmodule Logger.Translator do
|
||||
end)
|
||||
end
|
||||
|
||||
defp crash_neighbour(min_level, [{:pid, pid}, {:registered_name, []} | info]) do
|
||||
indent = " "
|
||||
@indent " "
|
||||
|
||||
[?\n, indent, inspect(pid) | crash_info(min_level, info, [?\n, indent | indent])]
|
||||
defp crash_neighbour(min_level, [{:pid, pid}, {:registered_name, []} | info]) do
|
||||
[?\n, @indent, inspect(pid) | crash_info(min_level, info, [?\n, @indent | @indent])]
|
||||
end
|
||||
|
||||
defp crash_neighbour(min_level, [{:pid, pid}, {:registered_name, name} | info]) do
|
||||
indent = " "
|
||||
|
||||
[?\n, indent, inspect(name), " (", inspect(pid), ")"] ++
|
||||
crash_info(min_level, info, [?\n, indent | indent])
|
||||
[?\n, @indent, inspect(name), " (", inspect(pid), ")"] ++
|
||||
crash_info(min_level, info, [?\n, @indent | @indent])
|
||||
end
|
||||
|
||||
defp format_stop({maybe_exception, [_ | _] = maybe_stacktrace} = reason) do
|
||||
defp format_last_message_from({_, {name, _}}), do: [" (from ", inspect(name), ")"]
|
||||
defp format_last_message_from({from, _}), do: [" (from ", inspect(from), ")"]
|
||||
defp format_last_message_from(_), do: []
|
||||
|
||||
defp format_client_info({from, :dead}),
|
||||
do: ["\nClient ", inspect(from), " is dead"]
|
||||
|
||||
defp format_client_info({from, :remote}),
|
||||
do: ["\nClient ", inspect(from), " is remote on node ", inspect(node(from))]
|
||||
|
||||
defp format_client_info({_, {name, stacktrace}}),
|
||||
do: ["\nClient ", inspect(name), " is alive\n" | format_stacktrace(stacktrace)]
|
||||
|
||||
defp format_client_info(_),
|
||||
do: []
|
||||
|
||||
defp format_reason({maybe_exception, [_ | _] = maybe_stacktrace} = reason) do
|
||||
try do
|
||||
format_stacktrace(maybe_stacktrace)
|
||||
catch
|
||||
:error, _ ->
|
||||
format_stop_banner(reason)
|
||||
{format_stop(reason), {reason, []}}
|
||||
else
|
||||
formatted_stacktrace ->
|
||||
[format_stop_banner(maybe_exception, maybe_stacktrace) | formatted_stacktrace]
|
||||
{formatted, reason} = maybe_normalize(maybe_exception, maybe_stacktrace)
|
||||
{[formatted | formatted_stacktrace], {reason, maybe_stacktrace}}
|
||||
end
|
||||
end
|
||||
|
||||
defp format_stop(reason) do
|
||||
format_stop_banner(reason)
|
||||
defp format_reason(reason) do
|
||||
{format_stop(reason), {reason, []}}
|
||||
end
|
||||
|
||||
defp format_stop_banner(reason) do
|
||||
defp format_stop(reason) do
|
||||
["\n** (stop) " | Exception.format_exit(reason)]
|
||||
end
|
||||
|
||||
# OTP processes rewrite the :undef error to these reasons when logging
|
||||
# Erlang processes rewrite the :undef error to these reasons when logging
|
||||
# TODO: This clause can be removed when we support only Erlang/OTP 21+.
|
||||
@gen_undef [:"module could not be loaded", :"function not exported"]
|
||||
|
||||
defp format_stop_banner(undef, [{mod, fun, args, _info} | _] = stacktrace)
|
||||
defp maybe_normalize(undef, [{mod, fun, args, _info} | _] = stacktrace)
|
||||
when undef in @gen_undef and is_atom(mod) and is_atom(fun) do
|
||||
cond do
|
||||
is_list(args) ->
|
||||
@@ -385,47 +512,44 @@ defmodule Logger.Translator do
|
||||
format_undef(mod, fun, args, undef, stacktrace)
|
||||
|
||||
true ->
|
||||
format_stop_banner(undef)
|
||||
{format_stop(undef), undef}
|
||||
end
|
||||
end
|
||||
|
||||
defp format_stop_banner(reason, stacktrace) do
|
||||
defp maybe_normalize(reason, stacktrace) do
|
||||
# If this is already an exception (even an ErlangError), we format it as an
|
||||
# exception. Otherwise, we try to normalize it, and if it's normalized as an
|
||||
# ErlangError we instead format it as an exit.
|
||||
if Exception.exception?(reason) do
|
||||
[?\n | Exception.format_banner(:error, reason, stacktrace)]
|
||||
{[?\n | Exception.format_banner(:error, reason, stacktrace)], reason}
|
||||
else
|
||||
case Exception.normalize(:error, reason, stacktrace) do
|
||||
%ErlangError{} ->
|
||||
format_stop_banner(reason)
|
||||
{format_stop(reason), reason}
|
||||
|
||||
exception ->
|
||||
[?\n | Exception.format_banner(:error, exception, stacktrace)]
|
||||
{[?\n | Exception.format_banner(:error, exception, stacktrace)], exception}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
defp format_undef(mod, fun, arity, undef, stacktrace) do
|
||||
opts = [module: mod, function: fun, arity: arity, reason: undef]
|
||||
exception = UndefinedFunctionError.exception(opts)
|
||||
[?\n | Exception.format_banner(:error, exception, stacktrace)]
|
||||
end
|
||||
|
||||
defp format(kind, payload, stacktrace) do
|
||||
[?\n, Exception.format_banner(kind, payload, stacktrace) | format_stacktrace(stacktrace)]
|
||||
end
|
||||
|
||||
defp format_stacktrace(stacktrace) do
|
||||
for entry <- stacktrace do
|
||||
[<<"\n ">> | Exception.format_stacktrace_entry(entry)]
|
||||
["\n " | Exception.format_stacktrace_entry(entry)]
|
||||
end
|
||||
end
|
||||
|
||||
defp format_mfa(mod, fun, :undefined),
|
||||
do: [inspect(mod), ?., Code.Identifier.inspect_as_function(fun) | "/?"]
|
||||
|
||||
defp format_mfa(mod, fun, args), do: Exception.format_mfa(mod, fun, args)
|
||||
defp format_mfa(mod, fun, args),
|
||||
do: Exception.format_mfa(mod, fun, args)
|
||||
|
||||
## Deprecated helpers
|
||||
|
||||
defp format_from([]), do: ""
|
||||
defp format_from([from]), do: " (from #{inspect(from)})"
|
||||
@@ -439,10 +563,16 @@ defmodule Logger.Translator do
|
||||
end
|
||||
|
||||
defp format_client([from, stacktrace]) when is_list(stacktrace) do
|
||||
["\nClient #{inspect(from)} is alive\n" | Exception.format_stacktrace(stacktrace)]
|
||||
["\nClient #{inspect(from)} is alive\n" | format_stacktrace(stacktrace)]
|
||||
end
|
||||
|
||||
defp format_client(_) do
|
||||
[]
|
||||
end
|
||||
|
||||
defp format_undef(mod, fun, arity, undef, stacktrace) do
|
||||
opts = [module: mod, function: fun, arity: arity, reason: undef]
|
||||
exception = UndefinedFunctionError.exception(opts)
|
||||
{[?\n | Exception.format_banner(:error, exception, stacktrace)], exception}
|
||||
end
|
||||
end
|
||||
|
||||
@@ -139,7 +139,7 @@ defmodule Logger.Utils do
|
||||
# arguments according to the truncate limit.
|
||||
{args, _} =
|
||||
Enum.map_reduce(args, truncate, fn arg, acc ->
|
||||
if is_binary(arg) do
|
||||
if is_binary(arg) and acc != :infinity do
|
||||
truncate_n(arg, acc)
|
||||
else
|
||||
{arg, acc}
|
||||
|
||||
@@ -20,13 +20,6 @@ defmodule Logger.Watcher do
|
||||
def init({mod, handler, args}) do
|
||||
Process.flag(:trap_exit, true)
|
||||
|
||||
# This is required for OTP 21. A better fix would be to not include
|
||||
# the error_logger handler in the first place but we will do so only
|
||||
# in future Elixir versions.
|
||||
unless Process.whereis(mod) do
|
||||
_ = :logger.add_handler(mod, mod, %{level: :info, filter_default: :log})
|
||||
end
|
||||
|
||||
case :gen_event.delete_handler(mod, handler, :ok) do
|
||||
{:error, :module_not_found} ->
|
||||
case :gen_event.add_sup_handler(mod, handler, args) do
|
||||
|
||||
@@ -24,7 +24,6 @@ defmodule Logger.ConfigTest do
|
||||
Process.whereis(Logger) |> Process.exit(:kill)
|
||||
wait_for_logger()
|
||||
wait_for_handler(Logger, Logger.Config)
|
||||
wait_for_handler(:error_logger, Logger.ErrorHandler)
|
||||
end
|
||||
|
||||
test "Logger.Config updates config on config_change/3" do
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
defmodule Logger.ErlangHandlerTest do
|
||||
use Logger.Case
|
||||
@moduletag :logger
|
||||
|
||||
defmodule CustomTranslator do
|
||||
def t(:debug, :info, :format, {'hello: ~p', [:ok]}) do
|
||||
:skip
|
||||
end
|
||||
|
||||
def t(:debug, :info, :format, {'world: ~p', [:ok]}) do
|
||||
{:ok, "rewritten"}
|
||||
end
|
||||
|
||||
def t(:debug, :info, :report, {:logger, %{hello: :ok}}) do
|
||||
:skip
|
||||
end
|
||||
|
||||
def t(:debug, :info, :report, {:logger, %{world: :ok}}) do
|
||||
{:ok, "rewritten"}
|
||||
end
|
||||
|
||||
def t(:debug, :info, :report, {:logger, %{error: error}}) do
|
||||
raise(error)
|
||||
end
|
||||
|
||||
def t(_, _, _, _) do
|
||||
:none
|
||||
end
|
||||
end
|
||||
|
||||
test "add_translator/1 and remove_translator/1 for error_logger" do
|
||||
assert Logger.add_translator({CustomTranslator, :t})
|
||||
|
||||
assert capture_log(fn ->
|
||||
:error_logger.info_msg('hello: ~p', [:ok])
|
||||
end) == ""
|
||||
|
||||
assert capture_log(fn ->
|
||||
:error_logger.info_msg('world: ~p', [:ok])
|
||||
end) =~ "[info] rewritten"
|
||||
after
|
||||
assert Logger.remove_translator({CustomTranslator, :t})
|
||||
end
|
||||
|
||||
test "add_translator/1 and remove_translator/1 for logger formats" do
|
||||
assert Logger.add_translator({CustomTranslator, :t})
|
||||
|
||||
assert capture_log(fn ->
|
||||
:logger.info('hello: ~p', [:ok])
|
||||
end) == ""
|
||||
|
||||
assert capture_log(fn ->
|
||||
:logger.info('world: ~p', [:ok])
|
||||
end) =~ "[info] rewritten"
|
||||
|
||||
assert capture_log(fn ->
|
||||
:logger.info(%{hello: :ok})
|
||||
end) == ""
|
||||
|
||||
assert capture_log(fn ->
|
||||
:logger.info(%{world: :ok})
|
||||
end) =~ "[info] rewritten"
|
||||
after
|
||||
assert Logger.remove_translator({CustomTranslator, :t})
|
||||
end
|
||||
|
||||
test "handles translation error" do
|
||||
assert Logger.add_translator({CustomTranslator, :t})
|
||||
|
||||
message = capture_log(fn -> :logger.info(%{error: "oops"}) end)
|
||||
assert message =~ "[info] Failure while translating Erlang's logger event\n"
|
||||
assert message =~ "** (RuntimeError) oops\n"
|
||||
after
|
||||
assert Logger.remove_translator({CustomTranslator, :t})
|
||||
end
|
||||
|
||||
test "converts Erlang metadata" do
|
||||
Logger.configure_backend(:console, metadata: [:file, :line, :module, :function])
|
||||
|
||||
message =
|
||||
capture_log(fn ->
|
||||
:logger.info("ok", %{file: 'file.erl', line: 13, mfa: {Foo, :bar, 3}})
|
||||
end)
|
||||
|
||||
assert message =~ "module=Foo"
|
||||
assert message =~ "function=bar/3"
|
||||
assert message =~ "file=file.erl"
|
||||
assert message =~ "line=13"
|
||||
after
|
||||
Logger.configure_backend(:console, metadata: [])
|
||||
end
|
||||
|
||||
test "uses reporting callback with Elixir inspection" do
|
||||
assert capture_log(fn ->
|
||||
callback = fn %{hello: :world} -> {"~p~n", [:formatted]} end
|
||||
:logger.info(%{hello: :world}, %{report_cb: callback})
|
||||
end) =~ "[info] :formatted"
|
||||
end
|
||||
|
||||
test "converts log levels" do
|
||||
assert capture_log(fn -> :logger.emergency('ok') end) =~ "[error] ok"
|
||||
assert capture_log(fn -> :logger.alert('ok') end) =~ "[error] ok"
|
||||
assert capture_log(fn -> :logger.critical('ok') end) =~ "[error] ok"
|
||||
assert capture_log(fn -> :logger.error('ok') end) =~ "[error] ok"
|
||||
assert capture_log(fn -> :logger.warning('ok') end) =~ "[warn] ok"
|
||||
assert capture_log(fn -> :logger.info('ok') end) =~ "[info] ok"
|
||||
assert capture_log(fn -> :logger.debug('ok') end) =~ "[debug] ok"
|
||||
end
|
||||
end
|
||||
@@ -1,5 +1,34 @@
|
||||
defmodule Logger.ErrorHandlerTest do
|
||||
use Logger.Case
|
||||
@moduletag :error_logger
|
||||
|
||||
test "add_translator/1 and remove_translator/1" do
|
||||
defmodule CustomTranslator do
|
||||
def t(:debug, :info, :format, {'hello: ~p', [:ok]}) do
|
||||
:skip
|
||||
end
|
||||
|
||||
def t(:debug, :info, :format, {'world: ~p', [:ok]}) do
|
||||
{:ok, "rewritten"}
|
||||
end
|
||||
|
||||
def t(_, _, _, _) do
|
||||
:none
|
||||
end
|
||||
end
|
||||
|
||||
assert Logger.add_translator({CustomTranslator, :t})
|
||||
|
||||
assert capture_log(fn ->
|
||||
:error_logger.info_msg('hello: ~p', [:ok])
|
||||
end) == ""
|
||||
|
||||
assert capture_log(fn ->
|
||||
:error_logger.info_msg('world: ~p', [:ok])
|
||||
end) =~ "\[info\] rewritten"
|
||||
after
|
||||
assert Logger.remove_translator({CustomTranslator, :t})
|
||||
end
|
||||
|
||||
test "survives after crashes" do
|
||||
expected_result =
|
||||
|
||||
@@ -339,15 +339,6 @@ defmodule Logger.TranslatorTest do
|
||||
"""s
|
||||
end
|
||||
|
||||
test "translates application stop" do
|
||||
assert capture_log(fn ->
|
||||
:ok = Application.start(:eex)
|
||||
Application.stop(:eex)
|
||||
end) =~ """
|
||||
Application eex exited: :stopped
|
||||
"""
|
||||
end
|
||||
|
||||
test "translates application start" do
|
||||
assert capture_log(fn ->
|
||||
Application.start(:eex)
|
||||
@@ -357,6 +348,15 @@ defmodule Logger.TranslatorTest do
|
||||
"""
|
||||
end
|
||||
|
||||
test "translates application stop" do
|
||||
assert capture_log(fn ->
|
||||
:ok = Application.start(:eex)
|
||||
Application.stop(:eex)
|
||||
end) =~ """
|
||||
Application eex exited: :stopped
|
||||
"""
|
||||
end
|
||||
|
||||
test "translates Process crashes" do
|
||||
assert capture_log(:info, fn ->
|
||||
{_, ref} = spawn_monitor(fn -> raise "oops" end)
|
||||
@@ -484,7 +484,7 @@ defmodule Logger.TranslatorTest do
|
||||
receive do: ({:DOWN, ^ref, _, _, _} -> :ok)
|
||||
end) =~ ~r"""
|
||||
Ancestors: \[#PID<\d+\.\d+\.\d+>\](?:
|
||||
Message Queue Length: 1(?#TODO: Require once depend on 20)|)
|
||||
Message Queue Length: 1(?#TODO: Require once depend on Erlang/OTP 20)|)
|
||||
Messages: \[:message\]
|
||||
Links: \[\]
|
||||
Dictionary: \[\]
|
||||
@@ -516,7 +516,7 @@ defmodule Logger.TranslatorTest do
|
||||
Stack Size: \d+
|
||||
Reductions: \d+(?:
|
||||
Current Stacktrace:
|
||||
test/logger/translator_test.exs:\d+: Logger.TranslatorTest.sleep/1(?#TODO: Require once depend on 20)|)
|
||||
test/logger/translator_test.exs:\d+: Logger.TranslatorTest.sleep/1(?#TODO: Require once depend on Erlang/OTP 20)|)
|
||||
"""
|
||||
end
|
||||
|
||||
@@ -775,7 +775,7 @@ defmodule Logger.TranslatorTest do
|
||||
"""
|
||||
end
|
||||
|
||||
test "handles :undefined MFA properly" do
|
||||
test "reports :undefined MFA properly" do
|
||||
defmodule WeirdFunctionNamesGenServer do
|
||||
use GenServer
|
||||
|
||||
|
||||
@@ -5,9 +5,9 @@ defmodule Logger.UtilsTest do
|
||||
|
||||
import Kernel, except: [inspect: 2]
|
||||
|
||||
defp inspect(format, args) do
|
||||
defp inspect(format, args, truncate \\ 10) do
|
||||
format
|
||||
|> Logger.Utils.scan_inspect(args, 10)
|
||||
|> Logger.Utils.scan_inspect(args, truncate)
|
||||
|> :io_lib.unscan_format()
|
||||
end
|
||||
|
||||
@@ -81,13 +81,13 @@ defmodule Logger.UtilsTest do
|
||||
assert truncate(long_string, :infinity) == long_string
|
||||
end
|
||||
|
||||
test "inspect/2 formats" do
|
||||
test "scan_inspect/3 formats" do
|
||||
assert inspect('~p', [1]) == {'~ts', [["1"]]}
|
||||
assert inspect("~p", [1]) == {'~ts', [["1"]]}
|
||||
assert inspect(:"~p", [1]) == {'~ts', [["1"]]}
|
||||
end
|
||||
|
||||
test "inspect/2 sigils" do
|
||||
test "scan_inspect/3 sigils" do
|
||||
assert inspect('~10.10tp', [1]) == {'~ts', [["1"]]}
|
||||
assert inspect('~-10.10tp', [1]) == {'~ts', [["1"]]}
|
||||
|
||||
@@ -95,24 +95,24 @@ defmodule Logger.UtilsTest do
|
||||
assert inspect('~10.10x~p~n', [1, 2, 3]) == {'~10.10x~ts~n', [1, 2, ["3"]]}
|
||||
end
|
||||
|
||||
test "inspect/2 with modifier t has no effect (as it is the default)" do
|
||||
test "scan_inspect/3 with modifier t has no effect (as it is the default)" do
|
||||
assert inspect('~tp', [1]) == {'~ts', [["1"]]}
|
||||
assert inspect('~tw', [1]) == {'~ts', [["1"]]}
|
||||
end
|
||||
|
||||
test "inspect/2 with modifier l always prints lists" do
|
||||
test "scan_inspect/3 with modifier l always prints lists" do
|
||||
assert inspect('~lp', ['abc']) == {'~ts', [["[", "97", ",", " ", "98", ",", " ", "99", "]"]]}
|
||||
assert inspect('~lw', ['abc']) == {'~ts', [["[", "97", ",", " ", "98", ",", " ", "99", "]"]]}
|
||||
end
|
||||
|
||||
test "inspect/2 with modifier for width" do
|
||||
test "scan_inspect/3 with modifier for width" do
|
||||
assert inspect('~5lp', ['abc']) ==
|
||||
{'~ts', [["[", "97", ",", "\n ", "98", ",", "\n ", "99", "]"]]}
|
||||
|
||||
assert inspect('~5lw', ['abc']) == {'~ts', [["[", "97", ",", " ", "98", ",", " ", "99", "]"]]}
|
||||
end
|
||||
|
||||
test "inspect/2 with modifier for limit" do
|
||||
test "scan_inspect/3 with modifier for limit" do
|
||||
assert inspect('~5lP', ['abc', 2]) ==
|
||||
{'~ts', [["[", "97", ",", "\n ", "98", ",", "\n ", "...", "]"]]}
|
||||
|
||||
@@ -120,13 +120,18 @@ defmodule Logger.UtilsTest do
|
||||
{'~ts', [["[", "97", ",", " ", "98", ",", " ", "...", "]"]]}
|
||||
end
|
||||
|
||||
test "inspect/2 truncates binaries" do
|
||||
test "scan_inspect/3 truncates binaries" do
|
||||
assert inspect('~ts', ["abcdeabcdeabcdeabcde"]) == {'~ts', ["abcdeabcde"]}
|
||||
|
||||
assert inspect('~ts~ts~ts', ["abcdeabcde", "abcde", "abcde"]) ==
|
||||
{'~ts~ts~ts', ["abcdeabcde", "", ""]}
|
||||
end
|
||||
|
||||
test "scan_inspect/3 with :infinity truncate" do
|
||||
long_string = String.duplicate("foo", 10000)
|
||||
assert inspect('~ts', [long_string], :infinity) == {'~ts', [long_string]}
|
||||
end
|
||||
|
||||
test "timestamp/1" do
|
||||
assert {{_, _, _}, {_, _, _, _}} = timestamp(true)
|
||||
end
|
||||
|
||||
@@ -4,44 +4,13 @@ defmodule LoggerTest do
|
||||
|
||||
setup_all do
|
||||
Logger.configure_backend(:console, metadata: [:application, :module])
|
||||
|
||||
on_exit(fn ->
|
||||
Logger.configure_backend(:console, metadata: [])
|
||||
end)
|
||||
on_exit(fn -> Logger.configure_backend(:console, metadata: []) end)
|
||||
end
|
||||
|
||||
defp msg_with_meta(text) do
|
||||
msg("module=LoggerTest #{text}")
|
||||
end
|
||||
|
||||
test "add_translator/1 and remove_translator/1" do
|
||||
defmodule CustomTranslator do
|
||||
def t(:debug, :info, :format, {'hello: ~p', [:ok]}) do
|
||||
:skip
|
||||
end
|
||||
|
||||
def t(:debug, :info, :format, {'world: ~p', [:ok]}) do
|
||||
{:ok, "rewritten"}
|
||||
end
|
||||
|
||||
def t(_, _, _, _) do
|
||||
:none
|
||||
end
|
||||
end
|
||||
|
||||
assert Logger.add_translator({CustomTranslator, :t})
|
||||
|
||||
assert capture_log(fn ->
|
||||
:error_logger.info_msg('hello: ~p', [:ok])
|
||||
end) == ""
|
||||
|
||||
assert capture_log(fn ->
|
||||
:error_logger.info_msg('world: ~p', [:ok])
|
||||
end) =~ "\[info\] rewritten"
|
||||
after
|
||||
assert Logger.remove_translator({CustomTranslator, :t})
|
||||
end
|
||||
|
||||
test "add_backend/1 and remove_backend/1" do
|
||||
assert :ok = Logger.remove_backend(:console)
|
||||
assert Application.get_env(:logger, :backends) == []
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
Logger.configure_backend(:console, colors: [enabled: false])
|
||||
ExUnit.start()
|
||||
|
||||
exclude = if Process.whereis(:logger), do: [:error_logger], else: [:logger]
|
||||
ExUnit.start(exclude: exclude)
|
||||
|
||||
defmodule Logger.Case do
|
||||
use ExUnit.CaseTemplate
|
||||
|
||||
@@ -84,7 +84,13 @@ defmodule Mix.Compilers.Elixir do
|
||||
stale_local_deps = stale_local_deps(manifest, modified)
|
||||
|
||||
{modules, structs, changed} =
|
||||
update_stale_entries(all_modules, all_sources, removed ++ changed, stale_local_deps, %{})
|
||||
update_stale_entries(
|
||||
all_modules,
|
||||
all_sources,
|
||||
removed ++ changed,
|
||||
stale_local_deps,
|
||||
stale_local_deps
|
||||
)
|
||||
|
||||
stale = changed -- removed
|
||||
|
||||
|
||||
@@ -18,6 +18,18 @@ defmodule Mix.Tasks.Cmd do
|
||||
mix cmd --app app1 --app app2 echo pwd
|
||||
|
||||
Aborts when a command exits with a non-zero status.
|
||||
|
||||
## Zombie OS processes
|
||||
|
||||
Beware that the Erlang VM does not terminate child processes
|
||||
when it shuts down. Therefore, if you use `mix cmd` to start
|
||||
long running processes and then shutdown the VM, it is likely
|
||||
that those child processes won't be terminated with the VM.
|
||||
|
||||
A solution is to make sure the child processes listen to the
|
||||
stdndard input and terminate when standard input is closed.
|
||||
We discuss this topic at length in the "Zombie OS processes"
|
||||
of the `Port` module documentation.
|
||||
"""
|
||||
|
||||
def run(args) do
|
||||
|
||||
@@ -352,7 +352,7 @@ defmodule Mix.Tasks.Format do
|
||||
|
||||
map =
|
||||
for input <- List.wrap(formatter_opts[:inputs]),
|
||||
file <- Path.wildcard(Path.join(prefix ++ [input])),
|
||||
file <- Path.wildcard(Path.join(prefix ++ [input]), match_dot: true),
|
||||
do: {file, formatter_opts},
|
||||
into: %{}
|
||||
|
||||
@@ -375,7 +375,7 @@ defmodule Mix.Tasks.Format do
|
||||
end
|
||||
|
||||
defp stdin_or_wildcard("-"), do: [:stdin]
|
||||
defp stdin_or_wildcard(path), do: Path.wildcard(path)
|
||||
defp stdin_or_wildcard(path), do: Path.wildcard(path, match_dot: true)
|
||||
|
||||
defp read_file(:stdin) do
|
||||
{IO.stream(:stdio, :line) |> Enum.to_list() |> IO.iodata_to_binary(), file: "stdin"}
|
||||
|
||||
@@ -7,7 +7,7 @@ defmodule Mix.Tasks.Run do
|
||||
Starts and runs the current application.
|
||||
|
||||
`mix run` can be used to start the current application dependencies
|
||||
ant the application itself. For long running systems, this is typically
|
||||
and the application itself. For long running systems, this is typically
|
||||
done with the `--no-halt` option:
|
||||
|
||||
mix run --no-halt
|
||||
|
||||
@@ -180,6 +180,34 @@ defmodule Mix.Tasks.FormatTest do
|
||||
end)
|
||||
end
|
||||
|
||||
test "expands patterns in inputs from .formatter.exs", context do
|
||||
in_tmp(context.test, fn ->
|
||||
File.write!(".formatter.exs", """
|
||||
[
|
||||
inputs: ["{a,.b}.ex"]
|
||||
]
|
||||
""")
|
||||
|
||||
File.write!("a.ex", """
|
||||
foo bar
|
||||
""")
|
||||
|
||||
File.write!(".b.ex", """
|
||||
foo bar
|
||||
""")
|
||||
|
||||
Mix.Tasks.Format.run([])
|
||||
|
||||
assert File.read!("a.ex") == """
|
||||
foo(bar)
|
||||
"""
|
||||
|
||||
assert File.read!(".b.ex") == """
|
||||
foo(bar)
|
||||
"""
|
||||
end)
|
||||
end
|
||||
|
||||
test "uses inputs and configuration from --dot-formatter", context do
|
||||
in_tmp(context.test, fn ->
|
||||
File.write!("custom_formatter.exs", """
|
||||
|
||||
@@ -326,7 +326,7 @@ defmodule Mix.UmbrellaTest do
|
||||
end
|
||||
end
|
||||
|
||||
test "recompiles after path dependency changes" do
|
||||
test "recompiles after runtime path dependency changes" do
|
||||
in_fixture("umbrella_dep/deps/umbrella/apps", fn ->
|
||||
Mix.Project.in_project(:bar, "bar", fn _ ->
|
||||
Mix.Task.run("compile", ["--verbose"])
|
||||
@@ -360,22 +360,51 @@ defmodule Mix.UmbrellaTest do
|
||||
# Noop for runtime dependencies
|
||||
mtime = File.stat!("_build/dev/lib/bar/.mix/compile.elixir").mtime
|
||||
ensure_touched("_build/dev/lib/foo/ebin/Elixir.Foo.beam", mtime)
|
||||
|
||||
mtime = File.stat!("_build/dev/lib/bar/.mix/compile.elixir").mtime
|
||||
ensure_touched("_build/dev/lib/foo/.mix/compile.elixir", mtime)
|
||||
|
||||
assert Mix.Tasks.Compile.Elixir.run(["--verbose"]) == {:noop, []}
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
test "recompiles after compile time path dependency changes" do
|
||||
in_fixture("umbrella_dep/deps/umbrella/apps", fn ->
|
||||
Mix.Project.in_project(:bar, "bar", fn _ ->
|
||||
Mix.Task.run("compile", ["--verbose"])
|
||||
|
||||
# Add compile time dependency
|
||||
File.write!("lib/bar.ex", "defmodule Bar, do: Foo.foo")
|
||||
|
||||
assert Mix.Tasks.Compile.Elixir.run(["--verbose"]) == {:ok, []}
|
||||
assert_receive {:mix_shell, :info, ["Compiled lib/bar.ex"]}
|
||||
|
||||
# Recompiles for compile time dependencies
|
||||
mtime = File.stat!("_build/dev/lib/bar/.mix/compile.elixir").mtime
|
||||
ensure_touched("_build/dev/lib/foo/ebin/Elixir.Foo.beam", mtime)
|
||||
ensure_touched("_build/dev/lib/foo/.mix/compile.elixir", mtime)
|
||||
|
||||
assert Mix.Tasks.Compile.Elixir.run(["--verbose"]) == {:ok, []}
|
||||
assert_receive {:mix_shell, :info, ["Compiled lib/bar.ex"]}
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
test "recompiles after struct path dependency changes" do
|
||||
in_fixture("umbrella_dep/deps/umbrella/apps", fn ->
|
||||
Mix.Project.in_project(:bar, "bar", fn _ ->
|
||||
File.write!("../foo/lib/foo.ex", "defmodule Foo, do: defstruct [:bar]")
|
||||
|
||||
Mix.Task.run("compile", ["--verbose"])
|
||||
|
||||
# Add struct dependency
|
||||
File.write!("lib/bar.ex", "defmodule Bar, do: %Foo{bar: true}")
|
||||
|
||||
assert Mix.Tasks.Compile.Elixir.run(["--verbose"]) == {:ok, []}
|
||||
assert_receive {:mix_shell, :info, ["Compiled lib/bar.ex"]}
|
||||
|
||||
# Recompiles for struct dependencies
|
||||
mtime = File.stat!("_build/dev/lib/bar/.mix/compile.elixir").mtime
|
||||
ensure_touched("_build/dev/lib/foo/ebin/Elixir.Foo.beam", mtime)
|
||||
ensure_touched("_build/dev/lib/foo/.mix/compile.elixir", mtime)
|
||||
|
||||
assert Mix.Tasks.Compile.Elixir.run(["--verbose"]) == {:ok, []}
|
||||
|
||||
Reference in New Issue
Block a user