yak(credo-strict-tuned-config-both-apps-x02d): credo strict, tuned config (both apps)

This commit is contained in:
Willem van den Ende
2026-10-29 10:13:02 +00:00
parent 13e3186f58
commit ef8407b272
13 changed files with 502 additions and 35 deletions
+44
View File
@@ -0,0 +1,44 @@
defmodule Blogex.LinkErrorTest do
@moduledoc """
Pins the rendered exception message, not just the struct.
The message is what a compile-time failure shows a human: the indentation and
the newline between errors are load-bearing. This is a characterization test
written before `message/1` was rewritten (`Enum.map/2 |> Enum.join/2` ->
`Enum.map_join/3`), so it passes on both sides of that change by design - it
exists to make the rewrite safe, not to prove new behaviour.
"""
use ExUnit.Case, async: true
test "message/1 renders one indented line per error" do
error = %Blogex.LinkError{
blog: :engineering,
post_id: "my-post",
errors: [
{1, "/blog/unknown/broken", "unknown blog ID: unknown", [post_id: "my-post"]},
{7, "/blog/engineering/", "empty slug", [post_id: "my-post"]}
]
}
assert Exception.message(error) == """
invalid internal blog links in engineering (post: my-post)
line 1: /blog/unknown/broken — unknown blog ID: unknown
line 7: /blog/engineering/ — empty slug
"""
end
test "message/1 omits the post label when there is no post id" do
error = %Blogex.LinkError{
blog: :engineering,
post_id: nil,
errors: [{3, "/blog/unknown/broken", "unknown blog ID: unknown", []}]
}
assert Exception.message(error) == """
invalid internal blog links in engineering
line 3: /blog/unknown/broken — unknown blog ID: unknown
"""
end
end
@@ -355,5 +355,12 @@ defmodule Blogex.LinkValidatorTest do
assert LinkValidator.parse_blog_link("/blog/engineering/hello-world#top") ==
{"engineering", "hello-world"}
end
# Only the paths that cannot be a blog link return nil; a nested slug
# ("/blog/eng/a/post") still parses and is rejected later by validate_slug/1.
test "returns nil for anything that is not /blog/<blog>/<slug>" do
refute LinkValidator.parse_blog_link("/blog/engineering")
refute LinkValidator.parse_blog_link("/notblog/engineering/a-post")
end
end
end
+66
View File
@@ -0,0 +1,66 @@
defmodule Blogex.CredoConfigTest do
@moduledoc """
Pins the *policy* in `.credo.exs`, not just the file's presence.
The gate is `mix credo --strict` (see the `credo` target in `Makefile`), so
the config is load-bearing: a check silently re-enabled here changes what
every future commit has to satisfy. These assertions are deliberately coarse
- "this check sits in that list", "the disable says why" - 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"
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
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
+10 -6
View File
@@ -10,17 +10,19 @@ defmodule Blogex.Test.AlphaBlog do
tests can assert exact orderings rather than re-deriving them.
"""
alias Blogex.Test.PostBuilder
def blog_id, do: :alpha
def all_posts,
do: [Blogex.Test.PostBuilder.build(id: "a1", date: ~D[2026-03-01], blog: :alpha)]
do: [PostBuilder.build(id: "a1", date: ~D[2026-03-01], blog: :alpha)]
def all_tags, do: ["elixir"]
def unfiltered_posts,
do: [
Blogex.Test.PostBuilder.build(id: "a1", date: ~D[2026-03-01], blog: :alpha),
Blogex.Test.PostBuilder.build(
PostBuilder.build(id: "a1", date: ~D[2026-03-01], blog: :alpha),
PostBuilder.build(
id: "a-draft",
date: ~D[2026-03-05],
blog: :alpha,
@@ -34,16 +36,18 @@ defmodule Blogex.Test.BetaBlog do
The second blog module for cross-blog tests; see `Blogex.Test.AlphaBlog`.
"""
alias Blogex.Test.PostBuilder
def blog_id, do: :beta
def all_posts,
do: [Blogex.Test.PostBuilder.build(id: "b1", date: ~D[2026-03-15], blog: :beta)]
do: [PostBuilder.build(id: "b1", date: ~D[2026-03-15], blog: :beta)]
def all_tags, do: ["devops"]
def unfiltered_posts,
do: [
Blogex.Test.PostBuilder.build(id: "b1", date: ~D[2026-03-15], blog: :beta),
Blogex.Test.PostBuilder.build(id: "b-future", date: ~D[2099-01-01], blog: :beta)
PostBuilder.build(id: "b1", date: ~D[2026-03-15], blog: :beta),
PostBuilder.build(id: "b-future", date: ~D[2099-01-01], blog: :beta)
]
end
+4 -2
View File
@@ -5,6 +5,8 @@ defmodule Blogex.Test.Setup do
import Blogex.Test.PostBuilder
alias Blogex.Test.FakeBlog
@doc """
Starts a FakeBlog with a standard set of posts.
Returns `%{blog: module, posts: posts}`.
@@ -13,9 +15,9 @@ defmodule Blogex.Test.Setup do
posts = Keyword.get(opts, :posts, default_posts())
blog_opts = Keyword.drop(opts, [:posts])
{:ok, _} = Blogex.Test.FakeBlog.start(posts, blog_opts)
{:ok, _} = FakeBlog.start(posts, blog_opts)
Map.merge(context, %{blog: Blogex.Test.FakeBlog, posts: posts})
Map.merge(context, %{blog: FakeBlog, posts: posts})
end
@doc "A small set of posts covering common scenarios."