Packages

XUnit Formatter for exunit test results. The format is tailored to work with azure devops, but any XUnit parser will work.

Current section

Files

Jump to
exunit_formatter_xunit lib xunit_formatter.ex
Raw

lib/xunit_formatter.ex

defmodule XUnitFormatter do
@moduledoc """
Documentation for `XUnitFormatter`.
"""
use GenServer
alias XUnitFormatter.Struct, as: XStruct
defstruct assembly: nil, collection: [], test_cases: [], skipped: %{}, config: %{}, date: nil
def expand_exception_paths(test = %ExUnit.Test{state: {:failed, failure}}, cwd, root_dir) do
%ExUnit.Test{
test
| state: {:failed, Enum.map(failure, &expand_exception_paths(&1, cwd, root_dir))},
tags: update_in(test.tags, [:file], &Path.relative_to(&1, root_dir))
}
end
def expand_exception_paths(test = %ExUnit.Test{}, _cwd, root_dir),
do: %ExUnit.Test{test | tags: update_in(test.tags, [:file], &Path.relative_to(&1, root_dir))}
def expand_exception_paths({kind, reason, stacktrace}, cwd, root_dir),
do: {kind, reason, Enum.map(stacktrace, &expand_exception_paths(&1, cwd, root_dir))}
def expand_exception_paths({module, fun, args, path}, cwd, root_dir),
do: {module, fun, args, expand_exception_paths(path, cwd, root_dir)}
def expand_exception_paths([file: path, line: line], cwd, root_dir) do
{path, cwd, root_dir}
if File.exists?(abspath = Path.expand(path, cwd)) do
[file: Path.relative_to(abspath, root_dir), line: line]
else
[file: path, line: line]
end
end
def expand_exception_paths(exception, _cwd, _root_dir) do
# Unrecognized exceptions should just be passed down verbatim
exception
end
@impl true
def init(config) do
xunit_report_dir = Application.get_env(:xunit_formatter, :report_dir, Mix.Project.app_path())
if Application.get_env(:xunit_formatter, :autocreate_report_dir, false) do
:ok = File.mkdir_p(xunit_report_dir)
end
config =
config
|> Keyword.put(:xunit_report_dir, xunit_report_dir)
|> Keyword.put(
:xunit_root_dir,
Application.get_env(:xunit_formatter, :root_dir, File.cwd!())
)
|> Keyword.put(
:xunit_prepend_app_name,
Application.get_env(:xunit_formatter, :prepend_app_name, false)
)
|> Enum.into(%{})
{:ok, %__MODULE__{config: config, date: DateTime.utc_now()}}
end
@impl true
def handle_cast({:suite_started, _opts}, state), do: {:noreply, %{state | assembly: nil}}
@impl true
def handle_cast({:suite_finished, %{async: _async, load: load, run: run}}, state) do
total = (run + (load || 0)) / 1_000_000
state = put_in(state.assembly.time, total)
prefix =
if state.config.xunit_prepend_app_name, do: "#{Mix.Project.config()[:app]}-", else: ""
filename = state.config.xunit_report_dir |> Path.join(prefix <> "xunit-report.xml")
xunit_data =
%XStruct.Document{assemblies: [state.assembly]}
|> XUnitFormatter.XUnitXML.xunit_xml()
File.write!(filename, xunit_data)
{:noreply, state}
end
@impl true
def handle_cast({:test_finished, test = %ExUnit.Test{state: {skipped, _}}}, state)
when skipped in [:skipped, :excluded, :invalid] do
skipped_tests_in_module = [test | Map.get(state.skipped, test.module, [])]
skipped_tests = Map.put(state.skipped, test.module, skipped_tests_in_module)
{:noreply, %{state | skipped: skipped_tests}}
end
@impl true
def handle_cast({:module_finished, test_module = %ExUnit.TestModule{}}, state) do
tests =
(test_module.tests ++ Map.get(state.skipped, test_module.name, []))
|> Enum.map(&expand_exception_paths(&1, File.cwd!(), state.config.xunit_root_dir))
|> Enum.map(&XStruct.Test.struct!/1)
module_time = test_module.tests |> Enum.reduce(0, fn test, acc -> acc + test.time end)
module_time = module_time / 1_000_000
collection = %XStruct.Collection{
name: "#{inspect(test_module.name)}",
time: module_time,
tests: tests
}
assembly =
if is_nil(state.assembly) do
name = test_module.file |> Path.relative_to(state.config.xunit_root_dir)
%XStruct.Assembly{name: name, environment: "seed=#{state.config.seed}"}
else
state.assembly
end
assembly = %{assembly | collections: [collection | assembly.collections]}
{:noreply, %{state | assembly: assembly}}
end
@impl true
def handle_cast({:sigquit, running_tests_and_modules}, state) do
# the VM is going to shutdown. It receives the test cases (or test module in case of setup_all) still running.
IO.inspect({:sigquit, running_tests_and_modules}, label: "xunit_formatter handle_cast")
{:noreply, state}
end
# Un-used casts
@impl true
def handle_cast({:module_started, _module = %ExUnit.TestModule{}}, state), do: {:noreply, state}
@impl true
def handle_cast({:test_started, _test = %ExUnit.Test{}}, state), do: {:noreply, state}
@impl true
def handle_cast({:test_finished, _test = %ExUnit.Test{}}, state), do: {:noreply, state}
# Deprecated casts
@impl true
def handle_cast({:case_started, _test_module}, state), do: {:noreply, state}
@impl true
def handle_cast({:case_finished, _test_module}, state), do: {:noreply, state}
end