defmodule AssertBoundary do @moduledoc """ ExUnit assertion helpers for testing module dependency boundaries. AssertBoundary uses Erlang's `:xref` to analyze BEAM bytecode and verify that module dependencies respect architectural boundaries. This lets you enforce that your runtime and verification pipelines don't leak into each other, that your domain layer stays independent, or any other structural invariant your architecture requires. ## Usage defmodule MyApp.BoundaryTest do use ExUnit.Case, async: true use AssertBoundary, app: :my_app test "web layer doesn't touch repo internals", %{boundary: boundary} do refute_calls(boundary, from: ~r/^MyApp\\.Web/, to: ~r/^MyApp\\.Repo\\.Internal/) end test "domain has restricted dependencies", %{boundary: boundary} do assert_boundary(boundary, modules: ~r/^MyApp\\.Domain/, allow: [~r/^MyApp\\.Schema/] ) end end `use AssertBoundary, app: :my_app` does three things: 1. Imports `AssertBoundary.Assertions` (`refute_calls/2`, `assert_calls/2`, `assert_boundary/2`). 2. Adds a `setup_all` that builds the dependency graph for the given app. 3. Makes the graph available as `%{boundary: graph}` in test context. """ alias AssertBoundary.Graph @doc false defmacro __using__(opts) do quote do import AssertBoundary.Assertions import AssertBoundary.Graph, only: [under: 1] setup_all do %{boundary: AssertBoundary.graph(unquote(opts))} end end end @doc """ Builds a dependency graph for the given application. Accepts a keyword list with an `:app` key, or an atom directly. ## Examples graph = AssertBoundary.graph(app: :my_app) graph = AssertBoundary.graph(:my_app) """ @spec graph(atom() | keyword()) :: Graph.t() def graph(opts) when is_list(opts) do app = Keyword.fetch!(opts, :app) Graph.build(app) end def graph(app) when is_atom(app) do Graph.build(app) end end