Skip to content

Instantly share code, notes, and snippets.

@Dkendal
Created February 3, 2021 21:43
Show Gist options
  • Save Dkendal/116063f23ab70ba764a0aa17a9fa3eb7 to your computer and use it in GitHub Desktop.
Save Dkendal/116063f23ab70ba764a0aa17a9fa3eb7 to your computer and use it in GitHub Desktop.
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