Packages

Spin up dynamic clusters in ExUnit tests with no special setup necessary.

Current section

Files

Jump to
ex_unit_cluster lib ex_unit_cluster.ex
Raw

lib/ex_unit_cluster.ex

defmodule ExUnitCluster do
@moduledoc """
Helpers for writing tests on nodes in isolated clusters.
This library relies on the [:peer](https://www.erlang.org/doc/man/peer.html)
module which was introduced in OTP 25.
## Examples
The most straightforward way to start using `ExUnitCluster` for a distributed test,
is to start `ExUnitCluster.Manager` inside the test.
defmodule SimpleTest do
use ExUnit.Case, async: true
test "start node in test case", ctx do
# 1) Start the cluster manager under the test supervisor
cluster = start_supervised!({ExUnitCluster.Manager, ctx})
# 2) Start a node linked to the given manager
node = ExUnitCluster.start_node(cluster)
# 3) Make an RPC to the node
node_name = ExUnitCluster.call(cluster, node, Node, :self, [])
refute Node.self() == node_name
end
end
If you want all tests in a module to use a cluster you can start `ExUnitCluster.Manager` in
`ExUnit.Callbacks.setup/1`.
defmodule ClusterTest do
use ExUnit.Case, async: true
setup ctx do
# 1) Start a cluster manager under the test supervisor for each test
cluster = start_supervised!({ExUnitCluster.Manager, ctx})
[cluster: cluster]
end
test "start node in test", %{cluster: cluster} do
# 2) Start a node in this test
node = ExUnitCluster.start_node(cluster)
# 3) Make an RPC to the node
node_name = ExUnitCluster.call(cluster, node, Node, :self, [])
refute Node.self() == node_name
end
end
Which is exactly what `ExUnitCluster.Case` does
defmodule ClusterTest do
use ExUnitCluster.Case, async: true
test "start node in test", %{cluster: cluster} do
node = ExUnitCluster.start_node(cluster)
node_name = ExUnitCluster.call(cluster, node, Node, :self, [])
refute Node.self() == node_name
end
end
"""
alias ExUnitCluster.Manager
@spec start_node(cluster :: pid(), timeout :: timeout()) :: node()
defdelegate start_node(pid, timeout \\ 30_000), to: Manager
@spec stop_node(cluster :: pid(), node :: node(), timeout :: timeout()) ::
:ok | {:error, :not_found}
defdelegate stop_node(pid, node, timeout \\ 5_000), to: Manager
@spec get_nodes(pid :: pid()) :: list(node())
defdelegate get_nodes(pid), to: Manager
@spec call(pid(), node(), module(), atom(), list(term()), timeout()) :: term()
defdelegate call(pid, node, module, function, args, timeout \\ 5_000), to: Manager
@doc """
Execute multiline code blocks on a specific node
"""
defmacro in_cluster(cluster, node, do: expressions) do
# We need a consistent random name, as this is compiled
# on each node separately at the moment.
module_name = :"#{:erlang.phash2(expressions)}"
quoted =
quote do
import ExUnit.Assertions
def run do
unquote(expressions)
end
end
Module.create(module_name, quoted, Macro.Env.location(__ENV__))
quote do
ExUnitCluster.call(unquote(cluster), unquote(node), unquote(module_name), :run, [])
end
end
end