lib/lcov_ex/stats.ex

defmodule LcovEx.Stats do
  @moduledoc """
  Output parser for `:cover.analyse/3`
  """
  @type cover_analyze_function_output :: [{{module(), atom(), integer()}, integer()}, ...]
  @type cover_analyze_line_output :: [{{module(), integer()}, integer()}, ...]
  @type coverage_info :: {binary(), integer()}

  @doc """
  Function coverage data parser. Discards BEAM file `:__info__/1` function data.

  ## Examples

      iex> LcovEx.Stats.function_coverage_data([{{MyModule, :__info__, 1}, 3}, {{MyModule, :foo, 2}, 0}])
      {[{"foo/2", 0}], %{fnf: 1, fnh: 0}}

  """
  @spec function_coverage_data(cover_analyze_function_output()) ::
          {[coverage_info(), ...], %{fnf: integer(), fnh: integer()}}
  def function_coverage_data(fun_data) do
    Enum.reduce_while(fun_data, {[], %{fnf: 0, fnh: 0}}, fn data,
                                                            acc = {list, %{fnf: fnf, fnh: fnh}} ->
      # TODO get FN + line by inspecting file
      case data do
        {{_, :__info__, _1}, _} ->
          {:cont, acc}

        {{_mod, name, arity}, count} ->
          {:cont,
           {list ++ [{"#{name}/#{arity}", count}],
            %{fnf: fnf + 1, fnh: fnh + ((count > 0 && 1) || 0)}}}
      end
    end)
  end

  @doc """
  Function coverage data parser. Discards BEAM file line `0` data.

  ## Examples

      iex> LcovEx.Stats.line_coverage_data([{{MyModule, 0}, 3}, {{MyModule, 0}, 0}, {{MyModule, 8}, 0}])
      {[{8, 0}], %{lf: 1, lh: 0}}

      iex> LcovEx.Stats.line_coverage_data([{{MyModule, 1}, 12}, {{MyModule, 1}, 0}, {{MyModule, 2}, 0}])
      {[{1, 12}, {2, 0}], %{lf: 2, lh: 1}}

  """
  @spec line_coverage_data(cover_analyze_line_output()) ::
          {[coverage_info(), ...], %{lf: integer(), lh: integer()}}
  def line_coverage_data(lines_data) do
    {list_reversed, _previous_line, lf, lh} =
      Enum.reduce(lines_data, {[], nil, 0, 0}, fn data, acc = {list, previous_line, lf, lh} ->
        case data do
          {{_, 0}, _} ->
            acc

          {^previous_line, count} ->
            [{line, previous_count} | rest] = list
            count = max(count, previous_count)

            lh = increment_line_hit(lh, count, previous_count)

            {[{line, count} | rest], previous_line, lf, lh}

          {{_mod, line} = previous_line, count} ->
            list = [{line, count} | list]
            lf = lf + 1
            lh = increment_line_hit(lh, count, 0)
            {list, previous_line, lf, lh}
        end
      end)

    {Enum.reverse(list_reversed), %{lf: lf, lh: lh}}
  end

  defp increment_line_hit(lh, count, previous_count)
  defp increment_line_hit(lh, 0, _), do: lh
  defp increment_line_hit(lh, _count, 0), do: lh + 1
  defp increment_line_hit(lh, _, _), do: lh
end