defmodule ExUnit.Formatter.StampFormatter do
use GenServer
@cell 5
@dir_path "/tmp/failures" # TODO hardcoded paths
@order_path "/tmp/test-order" # TODO hardcoded paths
defstruct [:width, count: 0, pos: %{}, failed: []]
@impl true
def init(_opts) do
pos = load_order()
{:ok, width} = :io.columns()
{:ok, %__MODULE__{width: width, pos: pos}}
end
defp report_failure({idx, [{:error, %ExUnit.AssertionError{}=ass, stack}]}) do
err_str = ExUnit.Formatter.format_assertion_error(ass)
stacktrace = stack
|> Enum.map(fn entry -> case entry do
{_, _, _, x} -> x
{_, _, x} -> x
end
end)
|> Enum.map(fn x -> line = Keyword.get(x, :line)
file = Keyword.get(x, :file)
"#{file}:#{line}"
end)
|> Enum.join("\n")
content = err_str <> "\n\n" <> stacktrace
path = "#{@dir_path}/#{idx}"
:ok = File.write!(path, content, [:write])
end
defp report_failure(_), do: nil
@impl true
def handle_cast({:suite_started, _opts}, state) do
IO.ANSI.clear
|> IO.puts
{:noreply, state}
end
def handle_cast({:suite_finished, _times_us}, %__MODULE__{}=state) do
IO.ANSI.default_color()
|> IO.puts
:ok = File.mkdir_p!(@dir_path)
{:ok, ls} = File.ls(@dir_path)
ls
|> Enum.map(& @dir_path <> "/" <> &1)
|> Enum.each(& File.rm!/1)
Enum.each(state.failed, &report_failure/1)
IO.puts("#{state.count} tests fin.")
save_order(state.pos)
{:noreply, state}
end
def handle_cast({:module_started, _test_module}, state) do
{:noreply, state}
end
def handle_cast({:module_finished, _test_module}, state) do
{:noreply, state}
end
def handle_cast({:test_started, %ExUnit.Test{module: m, name: n}},
%__MODULE__{}=state
) do
k = {m, n}
{id, state_} = if Map.has_key?(state.pos, k) do
id = state.pos[k]
{id, state}
else
id = state.count
state_ = %{state | pos: Map.put(state.pos, k, id)}
{id, state_}
end
{y, x} = divmod(id, state.width)
IO.ANSI.cursor(y, x * @cell + 1) <> to_string(id)
|> IO.puts
{:noreply, %{state_ | count: state.count + 1}}
end
def handle_cast({:test_finished, %ExUnit.Test{module: m, name: n, state: s}},
%__MODULE__{pos: map}=state
) do
i = map[{m, n}]
txt = i |> to_string |> String.pad_trailing(@cell)
stamp = case s do
nil -> IO.ANSI.green_background <> txt
{:excluded, _} -> ""
{:failed, _} -> IO.ANSI.red_background <> txt
{:invalid, _} -> IO.ANSI.crossed_out <> txt
{:skipped, _} -> IO.ANSI.yellow_background <> txt
end
{y, x} = divmod(i, state.width)
IO.ANSI.cursor(y, x * @cell + 1)
<> stamp
<> IO.ANSI.default_color() <> IO.ANSI.default_background()
|> IO.puts
{:noreply, state |> record_failure(i, s)}
end
def handle_cast({:sigquit, _remaining}, state) do
{:noreply, state}
end
# DEPRECATED
def handle_cast({:case_started, _}, x), do: {:noreply, x}
# DEPRECATED
def handle_cast({:case_finished, _}, x), do: {:noreply, x}
def handle_cast(_, state), do: {:noreply, state}
defp record_failure(state, i, {:failed, fail}) do
%{state | failed: [{i, fail} | state.failed]}
end
defp record_failure(state, _, _), do: state
defp save_order(pos) do
content = pos
|> Map.to_list()
|> Enum.sort_by(&elem(&1, 1))
|> Enum.map(&elem(&1, 0))
|> Enum.map(fn {m, n} -> "#{m} #{n}" end)
|> Enum.join("\n")
:ok = File.write!(@order_path, content, [:write])
end
defp load_order() do
path = "/tmp/test-order"
if File.exists?(path) do
path
|> File.read!()
|> String.trim_trailing()
|> String.split("\n")
|> Enum.map(fn s -> s |> String.split(" ", parts: 2) |> Enum.map(&String.to_atom/1) end)
|> Enum.map(fn [module, test] -> {module, test} end)
|> Enum.with_index()
|> Map.new
else
%{}
end
end
defp divmod(a, b) do
{floor(a / b), rem(a, b)}
end
end