Packages

ExUnit formatter that emits GitHub Actions annotations for failing tests.

Current section

Files

Jump to
github_actions_formatter lib github_actions_formatter.ex
Raw

lib/github_actions_formatter.ex

defmodule GithubActionsFormatter do
@moduledoc """
An `ExUnit.Formatter` that emits [GitHub Actions workflow commands][cmds]
so that failing tests show up as inline annotations within the PR diff.
ExUnit.start(formatters: [ExUnit.CLIFormatter, GithubActionsFormatter])
Auto-detects `GITHUB_ACTIONS=true`; override with
`config :github_actions_formatter, enabled: true`.
[cmds]: https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions
"""
@behaviour GenServer
@impl true
def init(_), do: {:ok, []}
@impl true
def handle_cast({:test_finished, %ExUnit.Test{state: {kind, _}} = test}, state)
when kind in [:failed, :invalid] do
with true <- enabled?(),
line when is_binary(line) <- annotation(test) do
IO.puts(line)
end
{:noreply, state}
end
def handle_cast(_, state), do: {:noreply, state}
defp enabled? do
Application.get_env(
:github_actions_formatter,
:enabled,
System.get_env("GITHUB_ACTIONS") == "true"
)
end
@doc false
def annotation(%ExUnit.Test{state: {_kind, failures}} = test) do
with file when is_binary(file) <- test.tags[:file],
line when is_integer(line) <- test.tags[:line] do
message = ExUnit.Formatter.format_test_failure(test, failures, 1, 80, fn _, m -> m end)
title = "#{inspect(test.module)} #{test.name}"
"::error file=#{prop(Path.relative_to_cwd(file))},line=#{line},title=#{prop(title)}::#{body(message)}"
else
_ -> nil
end
end
defp prop(value),
do: value |> escape() |> String.replace(":", "%3A") |> String.replace(",", "%2C")
defp body(value), do: escape(value)
defp escape(value) do
value
|> to_string()
|> String.replace("%", "%25")
|> String.replace("\r", "%0D")
|> String.replace("\n", "%0A")
end
end