lib/tasks/lcov.ex

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

  use Mix.Task
  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])

    if opts[:quiet], do: Mix.shell(Mix.Shell.Quiet)

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

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

    # Actually run tests and coverage
    args = Enum.join(args, " ")

    Mix.shell().cmd(
      "mix lcov.run #{args}",
      cd: path,
      env: [{"MIX_ENV", "test"}]
    )

    # Umbrella projects support
    if Mix.Project.umbrella?() do
      for {app, path} <- Mix.Project.apps_paths() do
        app_lcov_path = Path.join(path, file_path)
        app_lcov = app_lcov_path |> File.read!() |> String.replace("SF:", "SF:#{path}/")

        File.write!(file_path, app_lcov, [: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
  end

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