defmodule Firehose.CredoConfigTest do @moduledoc """ Pins the *policy* in `app/.credo.exs`, not just the file's presence. The gate is `mix credo --strict` (the `credo` target in `app/Makefile`, a prerequisite of `check`), so the config decides what every future commit has to satisfy. Plain `ExUnit.Case`: this reads and evaluates a file, it does not touch the database or boot the app's supervision tree. The assertions are deliberately coarse - "this check sits in that list", "the disable says why", "the custom check is still loaded" - so they survive credo's own boilerplate churn but not a policy change. """ use ExUnit.Case, async: true @config_path ".credo.exs" @complexity_check "Credo.Check.Refactor.CyclomaticComplexity" @custom_check "Firehose.Checks.NoConnShadowing" @custom_check_load_path "lib_dev/firehose/checks/" test "cyclomatic complexity is disabled in favour of CRAP" do checks = default_config()[:checks] assert @complexity_check in check_modules(checks[:disabled]) refute @complexity_check in check_modules(checks[:enabled]) end test "the disable carries a rationale comment" do rationale = rationale_above(@complexity_check) refute rationale == [], "no comment block above the #{@complexity_check} entry" assert Enum.join(rationale, "\n") =~ ~r/crap/i end # Regression guard: `mix credo.gen.config` would silently drop both of these, # and the custom check would stop running without one word of warning. test "the custom check and its load path survive" do config = default_config() assert @custom_check in check_modules(config[:checks][:enabled]) assert @custom_check_load_path in config[:requires] end defp default_config do {config, _bindings} = Code.eval_file(@config_path) Enum.find(config.configs, &(&1.name == "default")) end defp check_modules(checks) do Enum.map(checks, fn {module, _opts} -> module_name(module) module -> module_name(module) end) end # `Atom.to_string/1` keeps the "Elixir." prefix that `inspect/1` drops; the # assertions read better (and diff better) without it. defp module_name(module) do module |> Atom.to_string() |> String.replace_prefix("Elixir.", "") end # The contiguous `#` block immediately above the check's own line. The entry # line is the one that is not itself a comment: a comment mentioning the # check by name must not be mistaken for the entry. defp rationale_above(check_name) do lines = @config_path |> File.read!() |> String.split("\n") index = Enum.find_index(lines, fn line -> String.contains?(line, check_name) and not comment?(line) end) lines |> Enum.take(index) |> Enum.reverse() |> Enum.take_while(&comment?/1) end defp comment?(line), do: line |> String.trim() |> String.starts_with?("#") end