diff options
Diffstat (limited to 'test/support/cluster.ex')
| -rw-r--r-- | test/support/cluster.ex | 218 | 
1 files changed, 218 insertions, 0 deletions
| diff --git a/test/support/cluster.ex b/test/support/cluster.ex new file mode 100644 index 000000000..deb37f361 --- /dev/null +++ b/test/support/cluster.ex @@ -0,0 +1,218 @@ +defmodule Pleroma.Cluster do +  @moduledoc """ +  Facilities for managing a cluster of slave VM's for federated testing. + +  ## Spawning the federated cluster + +  `spawn_cluster/1` spawns a map of slave nodes that are started +  within the running VM. During startup, the slave node is sent all configuration +  from the parent node, as well as all code. After receiving configuration and +  code, the slave then starts all applications currently running on the parent. +  The configuration passed to `spawn_cluster/1` overrides any parent application +  configuration for the provided OTP app and key. This is useful for customizing +  the Ecto database, Phoenix webserver ports, etc. + +  For example, to start a single federated VM named ":federated1", with the +  Pleroma Endpoint running on port 4123, and with a database named +  "pleroma_test1", you would run: + +    endpoint_conf = Application.fetch_env!(:pleroma, Pleroma.Web.Endpoint) +    repo_conf = Application.fetch_env!(:pleroma, Pleroma.Repo) + +    Pleroma.Cluster.spawn_cluster(%{ +      :"federated1@127.0.0.1" => [ +        {:pleroma, Pleroma.Repo, Keyword.merge(repo_conf, database: "pleroma_test1")}, +        {:pleroma, Pleroma.Web.Endpoint, +        Keyword.merge(endpoint_conf, http: [port: 4011], url: [port: 4011], server: true)} +      ] +    }) + +  *Note*: application configuration for a given key is not merged, +  so any customization requires first fetching the existing values +  and merging yourself by providing the merged configuration, +  such as above with the endpoint config and repo config. + +  ## Executing code within a remote node + +  Use the `within/2` macro to execute code within the context of a remote +  federated node. The code block captures all local variable bindings from +  the parent's context and returns the result of the expression after executing +  it on the remote node. For example: + +      import Pleroma.Cluster + +      parent_value = 123 + +      result = +        within :"federated1@127.0.0.1" do +          {node(), parent_value} +        end + +      assert result == {:"federated1@127.0.0.1, 123} + +  *Note*: while local bindings are captured and available within the block, +  other parent contexts like required, aliased, or imported modules are not +  in scope. Those will need to be reimported/aliases/required within the block +  as `within/2` is a remote procedure call. +  """ + +  @extra_apps Pleroma.Mixfile.application()[:extra_applications] + +  @doc """ +  Spawns the default Pleroma federated cluster. + +  Values before may be customized as needed for the test suite. +  """ +  def spawn_default_cluster do +    endpoint_conf = Application.fetch_env!(:pleroma, Pleroma.Web.Endpoint) +    repo_conf = Application.fetch_env!(:pleroma, Pleroma.Repo) + +    spawn_cluster(%{ +      :"federated1@127.0.0.1" => [ +        {:pleroma, Pleroma.Repo, Keyword.merge(repo_conf, database: "pleroma_test_federated1")}, +        {:pleroma, Pleroma.Web.Endpoint, +         Keyword.merge(endpoint_conf, http: [port: 4011], url: [port: 4011], server: true)} +      ], +      :"federated2@127.0.0.1" => [ +        {:pleroma, Pleroma.Repo, Keyword.merge(repo_conf, database: "pleroma_test_federated2")}, +        {:pleroma, Pleroma.Web.Endpoint, +         Keyword.merge(endpoint_conf, http: [port: 4012], url: [port: 4012], server: true)} +      ] +    }) +  end + +  @doc """ +  Spawns a configured map of federated nodes. + +  See `Pleroma.Cluster` module documentation for details. +  """ +  def spawn_cluster(node_configs) do +    # Turn node into a distributed node with the given long name +    :net_kernel.start([:"primary@127.0.0.1"]) + +    # Allow spawned nodes to fetch all code from this node +    {:ok, _} = :erl_boot_server.start([]) +    allow_boot("127.0.0.1") + +    silence_logger_warnings(fn -> +      node_configs +      |> Enum.map(&Task.async(fn -> start_slave(&1) end)) +      |> Enum.map(&Task.await(&1, 60_000)) +    end) +  end + +  @doc """ +  Executes block of code again remote node. + +  See `Pleroma.Cluster` module documentation for details. +  """ +  defmacro within(node, do: block) do +    quote do +      rpc(unquote(node), unquote(__MODULE__), :eval_quoted, [ +        unquote(Macro.escape(block)), +        binding() +      ]) +    end +  end + +  @doc false +  def eval_quoted(block, binding) do +    {result, _binding} = Code.eval_quoted(block, binding, __ENV__) +    result +  end + +  defp start_slave({node_host, override_configs}) do +    log(node_host, "booting federated VM") +    {:ok, node} = :slave.start(~c"127.0.0.1", node_name(node_host), vm_args()) +    add_code_paths(node) +    load_apps_and_transfer_configuration(node, override_configs) +    ensure_apps_started(node) +    {:ok, node} +  end + +  def rpc(node, module, function, args) do +    :rpc.block_call(node, module, function, args) +  end + +  defp vm_args do +    ~c"-loader inet -hosts 127.0.0.1 -setcookie #{:erlang.get_cookie()}" +  end + +  defp allow_boot(host) do +    {:ok, ipv4} = :inet.parse_ipv4_address(~c"#{host}") +    :ok = :erl_boot_server.add_slave(ipv4) +  end + +  defp add_code_paths(node) do +    rpc(node, :code, :add_paths, [:code.get_path()]) +  end + +  defp load_apps_and_transfer_configuration(node, override_configs) do +    Enum.each(Application.loaded_applications(), fn {app_name, _, _} -> +      app_name +      |> Application.get_all_env() +      |> Enum.each(fn {key, primary_config} -> +        rpc(node, Application, :put_env, [app_name, key, primary_config, [persistent: true]]) +      end) +    end) + +    Enum.each(override_configs, fn {app_name, key, val} -> +      rpc(node, Application, :put_env, [app_name, key, val, [persistent: true]]) +    end) +  end + +  defp log(node, msg), do: IO.puts("[#{node}] #{msg}") + +  defp ensure_apps_started(node) do +    loaded_names = Enum.map(Application.loaded_applications(), fn {name, _, _} -> name end) +    app_names = @extra_apps ++ (loaded_names -- @extra_apps) + +    rpc(node, Application, :ensure_all_started, [:mix]) +    rpc(node, Mix, :env, [Mix.env()]) +    rpc(node, __MODULE__, :prepare_database, []) + +    log(node, "starting application") + +    Enum.reduce(app_names, MapSet.new(), fn app, loaded -> +      if Enum.member?(loaded, app) do +        loaded +      else +        {:ok, started} = rpc(node, Application, :ensure_all_started, [app]) +        MapSet.union(loaded, MapSet.new(started)) +      end +    end) +  end + +  @doc false +  def prepare_database do +    log(node(), "preparing database") +    repo_config = Application.get_env(:pleroma, Pleroma.Repo) +    repo_config[:adapter].storage_down(repo_config) +    repo_config[:adapter].storage_up(repo_config) + +    {:ok, _, _} = +      Ecto.Migrator.with_repo(Pleroma.Repo, fn repo -> +        Ecto.Migrator.run(repo, :up, log: false, all: true) +      end) + +    Ecto.Adapters.SQL.Sandbox.mode(Pleroma.Repo, :manual) +    {:ok, _} = Application.ensure_all_started(:ex_machina) +  end + +  defp silence_logger_warnings(func) do +    prev_level = Logger.level() +    Logger.configure(level: :error) +    res = func.() +    Logger.configure(level: prev_level) + +    res +  end + +  defp node_name(node_host) do +    node_host +    |> to_string() +    |> String.split("@") +    |> Enum.at(0) +    |> String.to_atom() +  end +end | 
