lib/ex_unit_notifier.ex

defmodule ExUnitNotifier do
  @moduledoc """
  Shows notifications for ExUnit test runs

  To enable notifications, add `ExUnitNotifier` as a formatter in your `test_helper.exs`:

      ExUnit.configure formatters: [ExUnit.CLIFormatter, ExUnitNotifier]

  """

  use GenServer

  alias ExUnitNotifier.Counter
  alias ExUnitNotifier.MessageFormatter

  @notifiers [
    ExUnitNotifier.Notifiers.TerminalNotifier,
    ExUnitNotifier.Notifiers.NotifySend,
    ExUnitNotifier.Notifiers.TmuxNotifier,
    ExUnitNotifier.Notifiers.TerminalTitle
  ]

  def init(_opts), do: {:ok, %Counter{}}

  def handle_cast({:test_finished, %ExUnit.Test{state: nil}}, counter),
    do: {:noreply, counter |> Counter.add_test()}

  def handle_cast({:test_finished, %ExUnit.Test{state: {:failed, _}}}, counter),
    do: {:noreply, counter |> Counter.add_test() |> Counter.add_failed()}

  def handle_cast({:test_finished, %ExUnit.Test{state: {:excluded, _}}}, counter),
    do: {:noreply, counter |> Counter.add_test() |> Counter.add_excluded()}

  def handle_cast({:test_finished, %ExUnit.Test{state: {:skipped, _}}}, counter),
    do: {:noreply, counter |> Counter.add_test() |> Counter.add_skipped()}

  def handle_cast({:test_finished, %ExUnit.Test{state: {:invalid, _}}}, counter),
    do: {:noreply, counter |> Counter.add_test() |> Counter.add_invalid()}

  # Elixir version < 1.12.0
  def handle_cast({:suite_finished, run_us, load_us}, counter) do
    apply(notifier(), :notify, [
      status(counter),
      MessageFormatter.format(counter, run_us, load_us),
      opts()
    ])

    {:noreply, counter}
  end

  # Elixir version >= 1.12.0, see https://hexdocs.pm/ex_unit/1.12.0/ExUnit.Formatter.html
  def handle_cast({:suite_finished, %{run: run_us, async: _async_us, load: load_us}}, counter) do
    apply(notifier(), :notify, [
      status(counter),
      MessageFormatter.format(counter, run_us, load_us),
      opts()
    ])

    {:noreply, counter}
  end

  def handle_cast(_, counter), do: {:noreply, counter}

  defp status(%Counter{failures: failures, invalid: invalid}) when failures > 0 or invalid > 0,
    do: :error

  defp status(_), do: :ok

  defp opts,
    do: %{
      clear_history: Application.get_env(:ex_unit_notifier, :clear_history, false)
    }

  defp notifier, do: Application.get_env(:ex_unit_notifier, :notifier, first_available_notifier())

  defp first_available_notifier,
    do: @notifiers |> Enum.find(fn notifier -> notifier.available? end)
end