defmodule XUnitFormatter.Struct do alias XUnitFormatter.XUnitXML defmodule Document do import XmlBuilder defstruct assemblies: [] defimpl XUnitXML do defdelegate attributes(data), to: XUnitXML.Any defdelegate child_elements(data), to: XUnitXML.Any def element_name(_data), do: :assemblies def content(data), do: Enum.map(data.assemblies, &XUnitXML.xunit_xml/1) def xunit_xml(data), do: document(element_name(data), content(data)) |> XmlBuilder.generate() end end defmodule RunDate do defstruct [:year, :month, :day] defimpl XUnitXML do defdelegate element_name(data), to: XUnitXML.Any defdelegate attributes(data), to: XUnitXML.Any defdelegate content(data), to: XUnitXML.Any defdelegate child_elements(data), to: XUnitXML.Any def xunit_xml(data) do now = DateTime.utc_now() yyyy = (data.year || now.year) |> Integer.to_string() |> String.pad_leading(4, "0") mm = (data.month || now.month) |> Integer.to_string() |> String.pad_leading(2, "0") dd = (data.day || now.day) |> Integer.to_string() |> String.pad_leading(2, "0") "#{yyyy}-#{mm}-#{dd}" end end end defmodule RunTime do defstruct [:hour, :minute, :second] defimpl XUnitXML do defdelegate element_name(data), to: XUnitXML.Any defdelegate attributes(data), to: XUnitXML.Any defdelegate content(data), to: XUnitXML.Any defdelegate child_elements(data), to: XUnitXML.Any def xunit_xml(data) do now = DateTime.utc_now() hh = (data.hour || now.hour) |> Integer.to_string() |> String.pad_leading(2, "0") mm = (data.minute || now.minute) |> Integer.to_string() |> String.pad_leading(2, "0") ss = (data.second || now.second) |> Integer.to_string() |> String.pad_leading(2, "0") "#{hh}:#{mm}:#{ss}" end end end defmodule Assembly do import XmlBuilder defstruct name: nil, config_file: nil, test_framework: :ex_unit, environment: nil, run_date: %RunDate{}, run_time: %RunTime{}, time: nil, total: nil, passed: 0, failed: 0, skipped: 0, errors: [], collections: [] def update_in(assembly = %Assembly{}, path, fun) do Kernel.struct!(Assembly, assembly |> Map.from_struct() |> Kernel.update_in(path, fun)) end defimpl XUnitXML do defdelegate xunit_xml(data), to: XUnitXML.Any defp add_test_numbers(collection, assembly) do %{total: total, passed: passed, failed: failed, skipped: skipped} = XUnitXML.attributes(collection) assembly |> Assembly.update_in([:total], &(&1 + total)) |> Assembly.update_in([:passed], &(&1 + passed)) |> Assembly.update_in([:failed], &(&1 + failed)) |> Assembly.update_in([:skipped], &(&1 + skipped)) end def attributes(data) do data.collections |> Enum.reduce(%{data | total: 0, passed: 0, failed: 0, skipped: 0}, &add_test_numbers/2) |> XUnitXML.Any.attributes() end def element_name(_), do: :assembly def child_elements(_), do: [:errors, :collections] def content(assembly) do errors = if is_list(assembly.errors) and length(assembly.errors) > 1 do [element(:errors, Enum.map(assembly.errors, &XUnitXML.xunit_xml/1))] else [] end collections = if is_list(assembly.collections) do Enum.map(assembly.collections, &XUnitXML.xunit_xml/1) else [] end errors ++ collections end end end defmodule Collection do @enforce_keys [:name] defstruct name: nil, time: nil, total: 0, passed: 0, failed: 0, skipped: 0, tests: [] defimpl XUnitXML do defdelegate xunit_xml(data), to: XUnitXML.Any def attributes(data) do %{ data | total: Enum.count(data.tests), passed: data.tests |> Enum.count(&XUnitFormatter.Struct.Test.passed?/1), failed: data.tests |> Enum.count(&XUnitFormatter.Struct.Test.failed?/1), skipped: data.tests |> Enum.count(&XUnitFormatter.Struct.Test.skipped?/1) } |> XUnitXML.Any.attributes() end def element_name(_), do: :collection def content(collection) do if is_list(collection.tests) do Enum.map(collection.tests, &XUnitXML.xunit_xml/1) else [] end end def child_elements(_), do: [:tests] end end defmodule Failure do defstruct exception_type: nil, message: nil, stack_trace: nil defp to_map({exception_type, message, stack_trace}), do: %{exception_type: exception_type, message: message, stack_trace: stack_trace} defp convert_exception([msg | _]), do: convert_exception(msg) defp convert_exception({type, %ExUnit.AssertionError{message: reason}, stack_trace}), do: {type, reason, Exception.format_stacktrace(stack_trace)} defp convert_exception({:error, reason, stack_trace}), do: {:error, Exception.message(reason), Exception.format_stacktrace(stack_trace)} defp convert_exception({type, reason, stack_trace}) when is_atom(type), do: {type, "#{inspect(reason)}", Exception.format_stacktrace(stack_trace)} defp convert_exception({type, reason, stack_trace}), do: {"#{inspect(type)}", "#{inspect(reason)}", Exception.format_stacktrace(stack_trace)} @spec struct!(ExUnit.failed()) :: %__MODULE__{} def struct!(failure) do Kernel.struct!(__MODULE__, failure |> convert_exception() |> to_map()) end defimpl XUnitXML do defdelegate attributes(data), to: XUnitXML.Any defdelegate xunit_xml(data), to: XUnitXML.Any def element_name(_), do: :failure def content(data) do [ message: {:cdata, data.message}, "stack-trace": {:cdata, data.stack_trace} ] end def child_elements(_), do: [:message, :stack_trace] end end defmodule Error do @enforce_keys [:name] defstruct name: nil, type: nil, total: nil, passed: nil, failed: nil, skipped: nil, failure: %Failure{} defimpl XUnitXML do defdelegate attributes(data), to: XUnitXML.Any defdelegate xunit_xml(data), to: XUnitXML.Any def element_name(_), do: :error def content(data), do: [XUnitXML.xunit_xml(data.failure)] def child_elements(_), do: [:failure] end end defmodule Result do defstruct result: "Pass", reason: nil, failure: nil def passed?(%__MODULE__{result: result}), do: result == "Pass" def failed?(%__MODULE__{result: result}), do: result == "Fail" def skipped?(%__MODULE__{result: result}), do: result == "Skip" def struct!(), do: __MODULE__.struct!(nil) def struct!(fields = %{}), do: Kernel.struct!(__MODULE__, fields) def struct!(nil), do: %__MODULE__{result: "Pass"} def struct!({:failed, reasons}), do: %__MODULE__{result: "Fail", failure: Failure.struct!(reasons)} def struct!({:invalid, reasons}), do: %__MODULE__{result: "Skip", reason: reasons} def struct!({:excluded, reasons}), do: %__MODULE__{result: "Skip", reason: reasons} def struct!({:skipped, reasons}), do: %__MODULE__{result: "Skip", reason: reasons} defimpl XUnitXML do def attributes(%Result{result: result}), do: %{result: result} def content(%Result{reason: reason, failure: nil}) when not is_nil(reason), do: [reason: {:cdata, reason}] def content(%Result{reason: nil, failure: failure}) when not is_nil(failure), do: [XUnitXML.xunit_xml(failure)] def content(_), do: [] defdelegate xunit_xml(data), to: XUnitXML.Any defdelegate element_name(data), to: XUnitXML.Any defdelegate child_elements(data), to: XUnitXML.Any end end defmodule Trait do defstruct [:name, :value] defimpl XUnitXML do defdelegate attributes(data), to: XUnitXML.Any defdelegate content(data), to: XUnitXML.Any defdelegate child_elements(data), to: XUnitXML.Any defdelegate xunit_xml(data), to: XUnitXML.Any def element_name(_), do: :trait end end defmodule Test do @enforce_keys [:name] defstruct name: nil, type: nil, method: nil, time: nil, traits: [], result: %Result{} def passed?(%__MODULE__{result: result}), do: Result.passed?(result) def failed?(%__MODULE__{result: result}), do: Result.failed?(result) def skipped?(%__MODULE__{result: result}), do: Result.skipped?(result) defp strip_test_header(title) when not is_binary(title), do: strip_test_header(to_string(title)) defp strip_test_header(<<"test ", rest::binary>>), do: rest defp strip_test_header(rest), do: rest defp string_or_inspect(value) do try do "#{value}" rescue Protocol.UndefinedError -> "#{inspect(value)}" ArgumentError -> "#{inspect(value)}" end end def struct!(test = %ExUnit.Test{}) do traits = test.tags |> Map.drop([:test_type, :case, :module, :test]) |> Enum.reject(&XUnitXML.Any.attribute_empty?/1) |> Enum.map(fn {k, v} -> %Trait{name: k, value: string_or_inspect(v)} end) Kernel.struct!(__MODULE__, %{ name: strip_test_header(test.name), method: strip_test_header(test.name), type: test.tags.test_type, time: test.time / 1_000_000, result: Result.struct!(test.state), traits: traits }) end defimpl XUnitXML do defdelegate xunit_xml(data), to: XUnitXML.Any def attributes(data) do attributes = XUnitXML.Any.attributes(data) result_attributes = XUnitXML.attributes(data.result) Map.merge(attributes, result_attributes) end def element_name(_), do: :test def content(data) do trait_content = if is_list(data.traits) and length(data.traits) > 1 do [traits: data.traits |> Enum.map(&XUnitXML.xunit_xml/1)] else [] end result_content = XUnitXML.content(data.result) result_content ++ trait_content end def child_elements(_), do: [:traits, :failure, :reason] end end end