lib/mix/tasks.ex

defmodule Mix.Tasks.Chaps do
  @moduledoc """
  A Mix task for gathering and displaying the current project's code coverage.
  """
  use Mix.Task

  @shortdoc "Gather and display the test coverage"
  @preferred_cli_env :test

  defmodule Runner do
    def run(task, args) do
      Mix.Task.run(task, args)
    end
  end

  def run(args) do
    {options, _, _} =
      OptionParser.parse(args, switches: [help: :boolean], aliases: [h: :help])

    if options[:help] do
      Chaps.Task.Util.print_help_message()
    else
      do_run(args, type: "local")
    end
  end

  @doc """
  Provides the logic to switch the parameters for Chaps.run/3.
  """
  def do_run(args, options) do
    if Mix.Project.config()[:test_coverage][:tool] != Chaps do
      raise Chaps.InvalidConfigError,
        message:
          "Please specify 'test_coverage: [tool: Chaps]' in the 'project' section of mix.exs"
    end

    switches = [
      filter: :string,
      umbrella: :boolean,
      verbose: :boolean,
      pro: :boolean,
      parallel: :boolean,
      sort: :string,
      output_dir: :string
    ]

    aliases = [f: :filter, u: :umbrella, v: :verbose, o: :output_dir]

    {args, common_options} =
      parse_common_options(args, switches: switches, aliases: aliases)

    all_options = options ++ common_options
    test_task = Mix.Project.config()[:test_coverage][:test_task] || "test"

    all_options =
      if all_options[:umbrella] do
        sub_apps = Chaps.SubApps.parse(Mix.Dep.Umbrella.loaded())

        all_options ++
          [sub_apps: sub_apps, apps_path: Mix.Project.config()[:apps_path]]
      else
        all_options
      end

    Chaps.ConfServer.start()
    Chaps.ConfServer.set(all_options ++ [args: args])
    Chaps.StatServer.start()

    Runner.run(test_task, ["--cover"] ++ args)

    if all_options[:umbrella] do
      type = options[:type] || "local"

      Chaps.StatServer.get()
      |> MapSet.to_list()
      |> get_stats(all_options)
      |> Chaps.analyze(type, options)
    end
  end

  def parse_common_options(args, common_options) do
    common_switches = Keyword.get(common_options, :switches, [])
    common_aliases = Keyword.get(common_options, :aliases, [])

    {common_options, _remaining, _invalid} =
      OptionParser.parse(args, common_options)

    # the switches that chaps supports
    supported_switches =
      Enum.map(Keyword.keys(common_switches), fn s ->
        String.replace("--#{s}", "_", "-")
      end) ++
        Enum.map(Keyword.keys(common_aliases), fn s -> "-#{s}" end)

    # Get the remaining args to pass onto cover, excluding Chaps-specific args.
    # Not using OptionParser for this because it splits things up in unfortunate ways.
    {remaining, _} =
      List.foldl(args, {[], nil}, fn arg, {acc, last} ->
        cond do
          # don't include switches for Chaps
          Enum.member?(supported_switches, arg) ->
            {acc, arg}

          # also drop any values that follow Chaps switches
          !String.starts_with?(arg, "-") &&
              Enum.member?(supported_switches, last) ->
            {acc, nil}

          # leaving just the switches and values intended for cover
          true ->
            {acc ++ [arg], nil}
        end
      end)

    sub_dir_set? = common_options[:subdir] not in [nil, ""]
    root_dir_set? = common_options[:rootdir] not in [nil, ""]

    if sub_dir_set? and root_dir_set? do
      raise Chaps.InvalidOptionError,
        message:
          "subdir and rootdir options are exclusive. please specify only one of them."
    end

    {remaining, common_options}
  end

  def get_stats(stats, options) do
    sub_dir_set? = options[:subdir] not in [nil, ""]
    root_dir_set? = options[:rootdir] not in [nil, ""]

    cond do
      sub_dir_set? ->
        stats
        |> Enum.map(fn m ->
          %{m | name: options[:subdir] <> Map.get(m, :name)}
        end)

      root_dir_set? ->
        stats
        |> Enum.map(fn m ->
          %{m | name: String.trim_leading(Map.get(m, :name), options[:rootdir])}
        end)

      true ->
        stats
    end
  end

  defmodule Detail do
    @moduledoc """
    Provides an entry point for displaying coveralls information
    with source code details.
    """
    use Mix.Task

    @shortdoc "Display the test coverage with source detail"
    @preferred_cli_env :test

    def run(args) do
      Mix.Tasks.Chaps.do_run(args, type: "local", detail: true)
    end
  end

  defmodule Html do
    @moduledoc """
    Provides an entry point for displaying coveralls information
    with source code details as an HTML report.
    """
    use Mix.Task

    @shortdoc "Display the test coverage with source detail as an HTML report"
    @preferred_cli_env :test

    def run(args) do
      Mix.Tasks.Chaps.do_run(args, type: "html")
    end
  end

  defmodule Xml do
    @moduledoc """
    Provides an entry point for outputting coveralls information
    as a XML file.
    """
    use Mix.Task

    @shortdoc "Output the test coverage as a XML file"
    @preferred_cli_env :test

    def run(args) do
      Mix.Tasks.Chaps.do_run(args, type: "xml")
    end
  end

  defmodule Json do
    @moduledoc """
    Provides an entry point for outputting coveralls information
    as a JSON file.
    """
    use Mix.Task

    @shortdoc "Output the test coverage as a JSON file"
    @preferred_cli_env :test

    def run(args) do
      Mix.Tasks.Chaps.do_run(args, type: "json")
    end
  end

  defmodule Lcov do
    @moduledoc """
    Provides an entry point for outputting coveralls information
    as a Lcov file.
    """
    use Mix.Task

    @shortdoc "Output the test coverage as a Lcov file"
    @preferred_cli_env :test

    def run(args) do
      Mix.Tasks.Chaps.do_run(args, type: "lcov")
    end
  end
end