Created
February 3, 2021 21:43
-
-
Save Dkendal/116063f23ab70ba764a0aa17a9fa3eb7 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule Debug do | |
alias Rexbug.Printing | |
alias Rexbug.Printing.Call | |
alias Rexbug.Printing.MFA | |
alias Rexbug.Printing.Return | |
import Inspect.Algebra | |
import Kernel, except: [inspect: 1, inspect: 2] | |
@up_right_arrow "↱" | |
@right_arrow "→" | |
@moduledoc """ | |
Debug helpers for test and development environments | |
""" | |
@syntax_colors [ | |
number: :yellow, | |
atom: :cyan, | |
string: :green, | |
boolean: :magenta, | |
nil: :magenta | |
] | |
@trace_colors [ | |
header: :inverse, | |
from_pid: :faint, | |
timestamp: :faint, | |
call: [:green, :inverse], | |
return: [:blue, :inverse], | |
stacktrace: :inverse | |
] | |
@doc """ | |
Pretty inspect `item`. | |
See `Inspect.Opts` for a full list of remaining formatting options. | |
""" | |
def inspect(term, opts \\ []) do | |
Kernel.inspect(term, merge_inspect_opts(opts)) | |
end | |
@doc """ | |
Pretty print given `item` to device. | |
See `IO.Inpsect/2` | |
See `Inspect.Opts` for a full list of remaining formatting options. | |
""" | |
@spec pp(item, keyword) :: item when item: var | |
def pp(item, opts \\ []) do | |
# credo:disable-for-next-line Credo.Check.Warning.IoInspect | |
IO.inspect(item, merge_inspect_opts(opts)) | |
end | |
@doc """ | |
Pretty prints `item` according to the given options using the IO device. | |
See pp/2 for a full list of options. | |
""" | |
@spec pp(IO.device(), item, keyword) :: item when item: var | |
def pp(device, item, opts) do | |
# credo:disable-for-next-line Credo.Check.Warning.IoInspect | |
IO.inspect(device, item, merge_inspect_opts(opts)) | |
end | |
@doc """ | |
See `Rexbug.start/2` for the options accepted for `trace_opts`. | |
See `Inspect.Opts` for a full list of remaining formatting options accepted by `merge_inspect_opts`. | |
# Examples | |
Calls, returns, and stacktrace | |
iex> {:ok, _} = trace("List :: stack;return") | |
iex> List.first([1, 2, 3]) | |
iex> trace_stop_sync() | |
:stopped | |
You can disable ansi colors by passing an empty array to `syntax_colors` | |
iex> trace ":maps", [], [syntax_colors: []] | |
""" | |
@spec trace(Rexbug.trace_pattern(), opts :: Keyword.t(), Inspect.Opts.t()) :: | |
Rexbug.rexbug_return() | |
def trace(pattern, trace_opts \\ [], inspect_opts \\ []) do | |
defaults = [ | |
print_fun: &trace_print_fun(&1, &2, inspect_opts) | |
] | |
trace_opts = Keyword.merge(trace_opts, defaults) | |
return = Rexbug.start(pattern, trace_opts) | |
case return do | |
{proc_count, func_count} when is_integer(proc_count) and is_integer(func_count) -> | |
{:ok, {proc_count, func_count}} | |
err -> | |
{:error, err} | |
end | |
end | |
defp trace_print_fun(trace_term, acc, opts) do | |
opts = merge_trace_inspect_opts(opts) | |
case Printing.from_erl(trace_term) do | |
%Return{} = term -> | |
IO.puts("#{inspect_return(term, opts)}") | |
%Call{} = term -> | |
IO.puts("#{inspect_call(term, opts)}") | |
term -> | |
pp(term, opts) | |
end | |
acc | |
end | |
@doc "See `Rexbug.stop/0`" | |
def trace_stop() do | |
Rexbug.stop() | |
end | |
@doc """ | |
See `Rexbug.stop_sync/1` | |
Useful when tracing in a test to stop tracing and print all the messages | |
before the test process exits. | |
""" | |
def trace_stop_sync(timeout \\ 100) do | |
Rexbug.stop_sync(timeout) | |
end | |
def inspect_return(%Return{} = item, opts) do | |
opts = struct(Inspect.Opts, opts) | |
mfa = inspect_mfa(item.mfa, opts) | |
return_value = Inspect.inspect(item.return_value, opts) | |
return = nest(glue(concat([mfa, " ", @right_arrow]), return_value), 2) | |
head = header(item, color("Return", :return, opts), opts) | |
doc = head |> line(return) |> nest(2) | |
concat([line(), doc, line()]) | |
|> format(80) | |
|> IO.iodata_to_binary() | |
end | |
defp timestamp(%{time: time}, opts) do | |
ts = time | |
{:ok, ts} = Time.new(ts.hours, ts.minutes, ts.seconds, {ts.us, 3}) | |
ts = Time.to_iso8601(ts) | |
color(string(ts), :timestamp, opts) | |
end | |
def from_pid(%{from_pid: pid}, opts) do | |
color(Inspect.inspect(pid, opts), :from_pid, opts) | |
end | |
def from_mfa(%{from_mfa: mfa}, opts) do | |
inspect_mfa(mfa, opts) | |
end | |
defp header(item, label, opts) do | |
timestamp(item, opts) | |
|> glue(label) | |
|> glue(from_mfa(item, opts)) | |
|> glue(from_pid(item, opts)) | |
|> group() | |
end | |
def inspect_call(%Call{} = item, opts) do | |
opts = struct(Inspect.Opts, opts) | |
stacktrace = | |
if item.dump != "" do | |
frames = inspect_stacktrace(item.dump, opts) | |
doc = "Stacktrace (most recent first):" | |
stack = | |
for frame <- frames, reduce: doc do | |
acc -> line(acc, group(glue(@up_right_arrow, frame))) | |
end | |
group(nest(stack, 2)) | |
else | |
empty() | |
end | |
call = inspect_mfa(item.mfa, opts) | |
head = header(item, color("Call", :call, opts), opts) | |
doc = head |> line(call) |> nest(2) |> line(stacktrace) | |
concat([line(), doc, line()]) | |
|> format(80) | |
|> IO.iodata_to_binary() | |
end | |
def inspect_mfa(%MFA{m: m, f: f, a: a}, opts) do | |
inspect_mfa(m, f, a, opts) | |
end | |
def inspect_mfa(term, opts) do | |
Inspect.inspect(term, opts) | |
end | |
def inspect_mfa(m, f, a, opts) do | |
doc = empty() | |
doc = concat(doc, Inspect.inspect(m, opts)) | |
doc = concat(doc, ".") | |
doc = concat(doc, string(to_string(f))) | |
cond do | |
is_integer(a) -> | |
concat([doc, "/", Inspect.inspect(a, opts)]) | |
is_list(a) -> | |
args_doc = container_doc("(", a, ")", opts, &Inspect.inspect/2) | |
concat(doc, args_doc) | |
end | |
end | |
defp merge_inspect_opts(opts) do | |
Keyword.merge( | |
[ | |
syntax_colors: @syntax_colors, | |
pretty: true, | |
limit: 10 | |
], | |
opts | |
) | |
end | |
defp merge_trace_inspect_opts(opts) do | |
Keyword.merge( | |
[ | |
syntax_colors: @syntax_colors ++ @trace_colors, | |
pretty: true, | |
limit: 10 | |
], | |
opts | |
) | |
end | |
defp inspect_stacktrace(dump, opts) do | |
String.split(dump, "\n") | |
|> Enum.filter(&Regex.match?(~r/Return addr 0x|CP: 0x/, &1)) | |
|> Enum.flat_map(&extract_function(&1, opts)) | |
end | |
defp extract_function(line, opts) do | |
case Regex.run(~r"^.+\((.+):(.+)/(\d+).+\)$", line, capture: :all_but_first) do | |
[m, f, a] -> | |
m = String.to_existing_atom(String.trim(m, "'")) | |
f = String.trim(f, "'") | |
a = String.to_integer(a) | |
[inspect_mfa(m, f, a, opts)] | |
nil -> | |
[] | |
end | |
end | |
def sigil_f(str, _opts) do | |
IO.ANSI.format(str, true) | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment