yak(mutation-testing-with-muex-blogex-pure-app-code-1341): mutation testing with muex (blogex + pure app code)

This commit is contained in:
Willem van den Ende
2026-10-29 10:07:55 +00:00
parent 09da15c1df
commit 3ef38bc8fc
14 changed files with 572 additions and 3 deletions
+49
View File
@@ -0,0 +1,49 @@
defmodule Firehose.MutationConfigTest do
# Same wiring pins as blogex, plus the app-specific one: because the app
# scope is deliberately empty (Q45), the aliases must fail loudly. A sensor
# that reports a score for a run that tested nothing is worse than no sensor.
use ExUnit.Case
test "muex is a dev/test-only, non-runtime dependency" do
deps = Mix.Project.config()[:deps]
assert {:muex, requirement, opts} = dep!(deps, :muex)
assert requirement =~ "0.12"
assert opts[:only] == [:dev, :test]
assert opts[:runtime] == false
end
test "mutate and mutate.fast aliases are defined" do
aliases = Mix.Project.config()[:aliases]
assert is_function(aliases[:mutate], 1)
assert is_function(aliases[:"mutate.fast"], 1)
end
test "an empty app scope fails loudly instead of running over nothing" do
assert_raise Mix.Error, ~r/empty/, fn ->
Firehose.MixProject.mutation_args("mutation_scope.txt", [])
end
end
test "ad-hoc args do not bypass the empty-scope failure" do
assert_raise Mix.Error, ~r/empty/, fn ->
Firehose.MixProject.mutation_args("mutation_scope.txt", [
"--files",
"lib/firehose/accounts/user.ex"
])
end
end
defp dep!(deps, name) do
case Enum.find(deps, fn
{^name, _requirement} -> true
{^name, _requirement, _opts} -> true
_ -> false
end) do
{^name, requirement} -> {name, requirement, []}
{^name, requirement, opts} -> {name, requirement, opts}
nil -> flunk("expected #{inspect(name)} in deps/0")
end
end
end
+53
View File
@@ -0,0 +1,53 @@
defmodule Firehose.MutationScopeTest do
# The app's candidate pure modules (Firehose.Accounts.User, Scope and
# UserToken) are only exercised through Firehose.DataCase: their tests use
# the SQL sandbox, fixtures and Repo. Per the yak's Q45 decision, an app
# mutation run would therefore need PostgreSQL, so the scope is deliberately
# empty and the alias fails loudly rather than silently running over nothing.
#
# These pins keep that decision visible: if the scope grows, this file must
# change with it, and it may never grow into the web layer.
use ExUnit.Case
@root Path.expand("..", __DIR__)
test "scope files exist" do
assert File.exists?(Path.join(@root, "mutation_scope.txt"))
assert File.exists?(Path.join(@root, "mutation_scope_fast.txt"))
end
test "app scope is empty because the candidate modules' tests need PostgreSQL" do
assert scope("mutation_scope.txt") == []
end
test "fast scope is a subset of full scope" do
out_of_scope =
MapSet.difference(
MapSet.new(scope("mutation_scope_fast.txt")),
MapSet.new(scope("mutation_scope.txt"))
)
assert MapSet.size(out_of_scope) == 0,
"mutate.fast names files outside the audited scope: #{inspect(MapSet.to_list(out_of_scope))}"
end
test "scope never includes web-layer code" do
web_entries =
Enum.filter(
scope("mutation_scope.txt") ++ scope("mutation_scope_fast.txt"),
&String.starts_with?(&1, "lib/firehose_web")
)
assert web_entries == [],
"the web layer (controllers, LiveViews, components) is out of scope: #{inspect(web_entries)}"
end
defp scope(file) do
@root
|> Path.join(file)
|> File.read!()
|> String.split("\n", trim: true)
|> Enum.map(&String.trim/1)
|> Enum.reject(&(&1 == ""))
end
end