lib/tasks/lcov.ex

defmodule Mix.Tasks.Lcov do
  @moduledoc "Generates lcov test coverage files for the application"
  @shortdoc "Generates lcov files"
  @preferred_cli_env :test

  use Mix.Task
  alias LcovEx.MixFileHelper
  require Logger

  @doc """
  Generates the `lcov.info` file.
  """
  @impl Mix.Task
  def run(args) do
    {opts, files} =
      OptionParser.parse!(args, strict: [quiet: :boolean, keep: :boolean, output: :string])

    path = Enum.at(files, 0) || File.cwd!()

    affected_files =
      case Mix.Project.apps_paths() do
        nil ->
          [Path.join([path, "mix.exs"])]

        apps_paths ->
          for {_app, app_path} <- apps_paths, do: Path.join([path, app_path, "mix.exs"])
      end
      |> Enum.map(fn path -> String.replace(path, "//", "/") end)

    Enum.each(affected_files, fn mix_path -> MixFileHelper.backup(mix_path) end)

    output = opts[:output] || "cover"
    file_path = "#{output}/lcov.info"
    File.mkdir_p!(output)
    File.rm(file_path)

    try do
      config = [test_coverage: [tool: LcovEx, output: output]]

      Enum.each(affected_files, fn mix_path ->
        MixFileHelper.update_project_config(mix_path, config)
      end)

      task_opts =
        if opts[:quiet] do
          [cd: path]
        else
          [cd: path, into: IO.stream(:stdio, :line)]
        end

      System.cmd("mix", ["test", "--cover"], task_opts)

      if Mix.Project.umbrella?() do
        for {app, path} <- Mix.Project.apps_paths() do
          app_lcov_path = Path.join(path, file_path)
          File.write!(file_path, File.read!(app_lcov_path), [:append])

          if opts[:keep] do
            log_info("Coverage file for #{app} created at #{app_lcov_path}", opts)
          else
            File.rm!(app_lcov_path)
          end
        end

        log_info("\nCoverage file for umbrella created at #{file_path}", opts)
      end

      :ok
    after
      Enum.each(affected_files, fn mix_path -> MixFileHelper.recover(mix_path) end)
    end
  end

  defp log_info(msg, opts) do
    unless Keyword.get(opts, :quiet, false) do
      Mix.shell().info(msg)
    end
  end
end