defmodule LcovEx do @moduledoc """ Lcov file generator for Elixir projects. Go to https://github.com/dariodf/lcov_ex for installation and usage instructions. """ alias LcovEx.{Formatter, Stats} def start(compile_path, opts) do log_info("Compiling coverage... ") :cover.start() case :cover.compile_beam_directory(compile_path |> to_charlist) do results when is_list(results) -> :ok {:error, _} -> Mix.raise("Failed to compile coverage for directory: " <> compile_path) end output = opts[:output] caller_cwd = opts[:cwd] || File.cwd!() ignored_paths = Keyword.get(opts, :ignore_paths, []) fn -> log_info("\nGenerating lcov file...") lcov = :cover.modules() |> Enum.sort() |> Enum.map(&calculate_module_coverage(&1, ignored_paths, caller_cwd)) File.mkdir_p!(output) path = "#{output}/lcov.info" File.write!(path, lcov, [:write]) inform_file_written(opts) :cover.stop() end end defp calculate_module_coverage(mod, ignored_paths, cwd) do path = mod.module_info(:compile)[:source] |> to_string() |> Path.relative_to(cwd) # Ignore compiled modules with path: # - not relative to the app (e.g. generated by umbrella dependencies) # - ignored by configuration if Path.type(path) != :relative or Enum.any?(ignored_paths, &String.starts_with?(path, &1)) do [] else calculate_and_format_coverage(mod, path) end end defp calculate_and_format_coverage(mod, path) do {:ok, fun_data} = :cover.analyse(mod, :calls, :function) {functions_coverage, %{fnf: fnf, fnh: fnh}} = Stats.function_coverage_data(fun_data) {:ok, lines_data} = :cover.analyse(mod, :calls, :line) {lines_coverage, %{lf: lf, lh: lh}} = Stats.line_coverage_data(lines_data) Formatter.format_lcov(mod, path, functions_coverage, fnf, fnh, lines_coverage, lf, lh) end defp inform_file_written(opts) do output = opts[:output] caller_cwd = opts[:cwd] || File.cwd!() path = "#{output}/lcov.info" keep? = opts[:keep] || false recursing? = Mix.Task.recursing?() app = Mix.Project.config()[:app] app_lcov_path = File.cwd!() |> Path.relative_to(caller_cwd) |> Path.join(path) cond do recursing? && keep? -> # Using --keep option from umbrella log_info("\nCoverage file for #{app} created at #{app_lcov_path}") not recursing? && File.cwd!() != caller_cwd -> # Using an umbrella app path from umbrella log_info("\nCoverage file created at #{app_lcov_path}") File.cwd!() == caller_cwd -> # Not an umbrella log_info("\nCoverage file created at #{path}") true -> # Don't log for umbrellas unless using --keep :no_log end end defp log_info(msg) do Mix.shell().info(msg) end end