lib/chaps/settings.ex

defmodule Chaps.Settings do
  strip_docs = fn subsection ->
    update_in(
      subsection,
      [
        Access.all(),
        Access.elem(1),
        Access.filter(fn {k, _v} -> k == :doc end)
      ],
      fn {:doc, _doc} -> {:doc, false} end
    )
  end

  @terminal_options [
    print_files: [
      doc: """
      Whether or not to print a table of files with the coverage of each
      file. If set to `false`, the table will not be printed but the overall
      coverage will be printed.
      """,
      type: :boolean,
      default: true
    ],
    file_column_width: [
      doc: """
      The width of the column to fit in file names in the terminal output
      table. Set this option to something larger like `80` or `100` if your
      project has files with long path names.
      """,
      type: :integer,
      default: 80
    ],
    filter_fully_covered: [
      doc: """
      Wether to filter fully covered modules from the terminal table.
      """,
      type: :boolean,
      default: true
    ]
  ]

  @coverage_options [
    treat_no_relevant_lines_as_covered: [
      doc: """
      Wether or not to treat files which have no significant lines of code
      as covered (`true`) at 100% or or not covered (`false`) at 0%.
      """,
      type: :boolean,
      default: true
    ],
    output_dir: [
      doc: """
      The output directory to place the HTML report, if generating an HTML
      report with `mix chaps.html`.
      """,
      type: :string,
      default: "cover"
    ],
    template_path: [
      doc: """
      A path to source a custom template for rendering HTML reports.
      Defaults to the internal EEx template within the Chaps library.
      """,
      type: :string
    ],
    xml_base_dir: [
      doc: """
      Where to output the XML report if generating an XML report.
      """,
      type: :string,
      default: ""
    ],
    minimum_coverage: [
      doc: """
      A percentage target for coverage. If the overall coverage falls below
      this target, the `mix chaps` tasks will exit with status code 1.
      Coverage is floored to the tenths place.
      """,
      type: :float,
      default: 100.0
    ],
    html_filter_fully_covered: [
      doc: """
      Wether or not to filter fully covered modules from the HTML source.
      """,
      type: :boolean,
      default: true
    ]
  ]

  @schema NimbleOptions.new!(
            default_stop_words: [
              doc: """
              A default set of words for which coverage should be ignored. This option
              should not be customized. Instead configure the `:custom_stop_words`
              key.
              """,
              type: {:list, {:custom, __MODULE__, :validate_regex, []}},
              default: [
                ~r/defmodule/,
                ~r/defrecord/,
                ~r/defimpl/,
                ~r/defexception/,
                ~r/defprotocol/,
                ~r/defstruct/,
                ~r/def.+(.+\\.+).+do/,
                ~r/^\s+use\s+/
              ]
            ],
            custom_stop_words: [
              doc: """
              A customizable set of words for which coverage should be ignored. The
              cover module has some difficulty discovering coverage on compile-time
              constructs in Elixir, so it can be useful to fully ignore some words.
              """,
              type: {:list, {:custom, __MODULE__, :validate_regex, []}},
              default: []
            ],
            skip_files: [
              doc: """
              A list of regular expressions to use to ignore matching path names from
              the coverage calculation.
              """,
              type: {:list, {:custom, __MODULE__, :validate_regex, []}},
              default: [~r/^test/, ~r/deps/]
            ],
            print_summary: [
              doc: """
              Whether or not to print the summary of the overall coverage to the
              terminal.
              """,
              type: :boolean,
              default: true
            ],
            terminal_options: [
              doc: """
              A set of terminal-output specific configuration options. See
              the Terminal Options section below for inner configuration.
              """,
              type: :keyword_list,
              keys: strip_docs.(@terminal_options),
              default: []
            ],
            coverage_options: [
              doc: """
              A set of options that configure how Chaps calculates coverage.
              See the Coverage Options section below for inner configuration.
              """,
              type: :keyword_list,
              keys: strip_docs.(@coverage_options),
              default: []
            ]
          )

  @moduledoc """
  Configurable options used for calculating and displaying coverage status

  ## Schema

  #{NimbleOptions.docs(@schema)}

  ### Terminal Options

  #{NimbleOptions.docs(@terminal_options)}

  ### Coverage Options

  #{NimbleOptions.docs(@coverage_options)}
  """

  @doc false
  def get_stop_words do
    read_config(:default_stop_words) ++ read_config(:custom_stop_words)
  end

  @doc false
  def get_coverage_options, do: read_config(:coverage_options)

  @doc false
  def default_coverage_value do
    if get_coverage_options()[:treat_no_relevant_lines_as_covered],
      do: 100.0,
      else: 0.0
  end

  @doc false
  def get_terminal_options, do: read_config(:terminal_options)

  @doc false
  def get_file_col_width do
    get_terminal_options()[:file_column_width]
  end

  @doc false
  def get_print_files do
    get_terminal_options()[:print_files]
  end

  @doc false
  def get_terminal_filter_fully_covered do
    get_terminal_options()[:filter_fully_covered]
  end

  @doc false
  def get_xml_base_dir do
    get_coverage_options()[:xml_base_dir]
  end

  @doc false
  def get_skip_files do
    read_config(:skip_files)
  end

  @doc false
  def get_print_summary do
    read_config(:print_summary)
  end

  @doc false
  def read_config(key) do
    :chaps
    |> Application.get_all_env()
    |> NimbleOptions.validate!(@schema)
    |> Keyword.fetch!(key)
  end

  @doc false
  def validate_regex(%Regex{} = regex), do: {:ok, regex}
  def validate_regex(binary) when is_binary(binary), do: Regex.compile(binary)

  def validate_regex(other),
    do: {:error, "must be a regular expression, got: #{inspect(other)}"}
end