defmodule ExUnitJsonFormatter do
@moduledoc """
Documentation for `ExUnitJsonFormatter`.
For another approach that inspired some of this work, see [junit_formatter](https://github.com/victorolinasc/junit-formatter).
"""
require Record
use GenServer
Record.defrecord(:jsonformat,
modules: [],
seed: nil,
run_time_usecs: nil,
async_time_usecs: nil,
load_time_usecs: nil,
sync_time_usecs: nil,
total_time_usecs: nil
)
@type jsonformat ::
record(:jsonformat,
modules: [ExUnit.TestModule.t()],
seed: any,
run_time_usecs: non_neg_integer,
async_time_usecs: non_neg_integer,
load_time_usecs: non_neg_integer,
sync_time_usecs: non_neg_integer,
total_time_usecs: non_neg_integer
)
@impl true
def init(opts) do
{:ok, jsonformat(seed: opts[:seed])}
end
@impl true
def handle_cast({:suite_started, _opts}, state) do
# IO.inspect("Suite started")
{:noreply, state}
end
@impl true
def handle_cast(
{:suite_finished,
%{
run: run_time_usecs,
async: async_time_usecs,
load: load_time_usecs
}},
state
) do
# IO.inspect("Suite finished, elapsed usecs #{total_time_usecs}")
sync_time_usecs = run_time_usecs - (async_time_usecs || 0)
total_time_usecs = run_time_usecs + (load_time_usecs || 0)
out_state =
jsonformat(state,
run_time_usecs: run_time_usecs,
async_time_usecs: async_time_usecs,
load_time_usecs: load_time_usecs,
sync_time_usecs: sync_time_usecs,
total_time_usecs: total_time_usecs
)
out_state
|> state_to_map()
|> Jason.encode!(pretty: true)
|> IO.puts()
{:noreply, out_state}
end
@impl true
def handle_cast({:module_started, %ExUnit.TestModule{} = _test_module}, state) do
# IO.inspect("Module started, #{test_module.name}")
{:noreply, state}
end
@impl true
def handle_cast(
{:module_finished, %ExUnit.TestModule{} = test_module},
jsonformat(modules: modules)
) do
# IO.inspect("Module finished, #{test_module.name}")
{:noreply, jsonformat(modules: [test_module | modules])}
end
def handle_cast({:test_started, %ExUnit.Test{state: nil} = _test}, config) do
# IO.inspect("Test started, #{test.module} #{test.name}")
{:noreply, config}
end
def handle_cast({:test_finished, %ExUnit.Test{state: nil} = _test}, config) do
# IO.inspect("Test finished, #{test.module} #{test.name}")
{:noreply, config}
end
def handle_cast({:test_finished, %ExUnit.Test{state: {:skip, _}} = _test}, config) do
# IO.inspect("Test skipped, #{test.module} #{test.name}")
{:noreply, config}
end
def handle_cast({:test_finished, %ExUnit.Test{state: {:excluded, _}} = _test}, config) do
# IO.inspect("Test excluded, #{test.module} #{test.name}")
{:noreply, config}
end
def handle_cast({:test_finished, %ExUnit.Test{state: {:failed, _failed}} = _test}, config) do
# IO.inspect("Test failed, #{test.module} #{test.name}")
{:noreply, config}
end
def handle_cast({:test_finished, %ExUnit.Test{state: {:invalid, _module}} = _test}, config) do
# IO.inspect("Test invalid, #{test.module} #{test.name}")
{:noreply, config}
end
def handle_cast({:sigquit, _}, state), do: {:noreply, state}
# per docs, we support these but ignore them
def handle_cast({:case_started, _test_module}, state), do: {:noreply, state}
def handle_cast({:case_finished, _test_module}, state), do: {:noreply, state}
def state_to_map(
jsonformat(
modules: modules,
seed: seed,
run_time_usecs: run_time_usecs,
async_time_usecs: async_time_usecs,
load_time_usecs: load_time_usecs,
sync_time_usecs: sync_time_usecs,
total_time_usecs: total_time_usecs
)
) do
summarized_modules = Enum.map(modules, &summarize_test_module/1)
%{
seed: seed,
run_time_usecs: run_time_usecs,
async_time_usecs: async_time_usecs,
load_time_usecs: load_time_usecs,
sync_time_usecs: sync_time_usecs,
total_time_usecs: total_time_usecs,
modules: summarized_modules
}
end
def summarize_test_module(%ExUnit.TestModule{} = test_module) do
module_state = decode_test_status(test_module.state)
summarized_tests = Enum.map(test_module.tests, &summarize_test_case/1)
%{
name: test_module.name,
state: module_state,
file: test_module.file,
test: summarized_tests
}
end
def summarize_test_case(%ExUnit.Test{} = test) do
test_state = decode_test_status(test.state)
line = test.tags.line
file = test.tags.file
%{
name: test.name,
module: test.module,
state: test_state,
time: test.time,
file: file,
line: line,
tags: test.tags
}
end
def decode_test_status(status) do
case status do
nil -> :passed
{:excluded, _} -> :skipped
{:failed, _} -> :failed
{:invalid, _} -> :invalid
{:skipped, _} -> :excluded
end
end
end