defmodule Blogex.MutationScopeTest do # The mutation scope files are the single source of truth for what `mix # mutate` / `mix mutate.fast` are allowed to touch. These pins keep the # sensor honest: a scope that quietly shrinks to an empty (or stale) set # would report a score for a check it never ran. use ExUnit.Case @root Path.expand("..", __DIR__) @full "mutation_scope.txt" @fast "mutation_scope_fast.txt" # Pure modules only. These are excluded deliberately: # blogex.ex facade + moduledoc, nothing to mutate # blogex/blog.ex the `__using__` macro (NimblePublisher expansion) # router.ex Phoenix router DSL # components.ex Phoenix component DSL # layout.ex Phoenix/LiveView layout DSL @shell_modules [ "lib/blogex.ex", "lib/blogex/blog.ex", "lib/blogex/router.ex", "lib/blogex/components.ex", "lib/blogex/layout.ex" ] test "full scope lists only existing files" do missing = Enum.reject(scope(@full), &File.exists?(Path.join(@root, &1))) assert missing == [], "mutation scope names files that do not exist: #{inspect(missing)}" end test "full scope is not empty" do assert scope(@full) != [] end test "fast scope is not empty" do assert scope(@fast) != [] end test "fast scope is a subset of full scope" do out_of_scope = MapSet.difference(MapSet.new(scope(@fast)), MapSet.new(scope(@full))) 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 excludes the macro and shell modules" do scoped = MapSet.new(scope(@full) ++ scope(@fast)) present = Enum.filter(@shell_modules, &MapSet.member?(scoped, &1)) assert present == [], "shell/macro modules belong outside the mutation scope: #{inspect(present)}" 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