Skip to content

Commit

Permalink
Generate code required to mock Erlang modules
Browse files Browse the repository at this point in the history
  • Loading branch information
pmeinhardt committed Dec 25, 2020
1 parent 839c28c commit ef90bcb
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 78 deletions.
2 changes: 1 addition & 1 deletion lib/sshkit/connection.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ defmodule SSHKit.Connection do
A connection struct has the following fields:
* `host` - the name or IP of the remote host
* `port` - the port to connect to
* `port` - the port connected to
* `options` - additional connection options
* `ref` - the underlying `:ssh` connection ref
"""
Expand Down
4 changes: 3 additions & 1 deletion test/support/functional_case.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ defmodule SSHKit.FunctionalCase do
@moduletag :functional

setup do
# Stub mocks with implementations delegating to the proper Erlang modules
# Stub mocks with implementations delegating to the original Erlang
# modules, essentially "unmocking" them unless explicit expectations
# are set up.
Mox.stub_with(MockErlangSsh, ErlangSsh)
Mox.stub_with(MockErlangSshConnection, ErlangSshConnection)
Mox.stub_with(MockErlangSshSftp, ErlangSshSftp)
Expand Down
61 changes: 61 additions & 0 deletions test/support/gen.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
defmodule Gen do
@moduledoc false

@doc """
Generates a behaviour based on an existing module.
Mox requires a behaviour to be defined in order to create a mock. To mock
core modules in tests - e.g. :ssh, :ssh_connection and :ssh_sftp - we need
behaviours mirroring their public API.
"""
def defbehaviour(name, target) when is_atom(name) and is_atom(target) do
info = moduledoc("Generated behaviour for #{inspect(target)}.")

body =
for {fun, arity} <- functions(target) do
args = 0..arity |> Enum.map(fn _ -> {:term, [], []} end) |> tl()

quote do
@callback unquote(fun)(unquote_splicing(args)) :: term()
end
end

Module.create(name, info ++ body, Macro.Env.location(__ENV__))
end

@doc """
Generates a module delegating all function calls to another module.
Mox requires modules used for stubbing to implement the mocked behaviour. To
mock core modules without behaviour definitions, we generate stand-in modules
which delegate
"""
def defdelegated(name, target, options \\ [])
when is_atom(name) and is_atom(target) and is_list(options) do
info =
moduledoc("Generated stand-in module for #{inspect(target)}.") ++
behaviour(Keyword.get(options, :behaviour))

body =
for {fun, arity} <- functions(target) do
args = Macro.generate_arguments(arity, name)

quote do
defdelegate unquote(fun)(unquote_splicing(args)), to: unquote(target)
end
end

Module.create(name, info ++ body, Macro.Env.location(__ENV__))
end

defp functions(module) do
exports = module.module_info(:exports)
Keyword.drop(exports, ~w[__info__ module_info]a)
end

defp moduledoc(nil), do: []
defp moduledoc(docstr), do: [quote(do: @moduledoc(unquote(docstr)))]

defp behaviour(nil), do: []
defp behaviour(name), do: [quote(do: @behaviour(unquote(name)))]
end
86 changes: 10 additions & 76 deletions test/support/mocks.ex
Original file line number Diff line number Diff line change
@@ -1,79 +1,13 @@
defmodule ErlangSshBehaviour do
@moduledoc false
require Gen

@type conn() :: term()
Gen.defbehaviour(ErlangSsh.Behaviour, :ssh)
Gen.defdelegated(ErlangSsh, :ssh, behaviour: ErlangSsh.Behaviour)
Mox.defmock(MockErlangSsh, for: ErlangSsh.Behaviour)

@callback connect(binary(), integer(), keyword(), timeout()) :: {:ok, conn()} | {:error, term()}
@callback close(conn()) :: :ok
end
Gen.defbehaviour(ErlangSshConnection.Behaviour, :ssh_connection)
Gen.defdelegated(ErlangSshConnection, :ssh_connection, behaviour: ErlangSshConnection.Behaviour)
Mox.defmock(MockErlangSshConnection, for: ErlangSshConnection.Behaviour)

defmodule ErlangSsh do
@moduledoc false

@behaviour ErlangSshBehaviour

defdelegate connect(host, port, options, timeout), to: :ssh
defdelegate close(conn), to: :ssh
end

Mox.defmock(MockErlangSsh, for: ErlangSshBehaviour)

defmodule ErlangSshConnectionBehaviour do
@moduledoc false

@type conn() :: term()
@type chan() :: integer()

@callback session_channel(conn(), integer(), integer(), timeout()) ::
{:ok, chan()} | {:error, term()}
@callback subsystem(conn(), chan(), charlist(), timeout()) ::
:success | :failure | {:error, :timeout} | {:error, :closed}
@callback close(conn(), chan()) :: :ok
@callback exec(conn(), chan(), binary(), timeout()) ::
:success | :failure | {:error, :timeout} | {:error, :closed}
@callback ptty_alloc(conn(), chan(), keyword(), timeout()) ::
:success | :failure | {:error, :timeout} | {:error, :closed}
@callback send(conn(), chan(), 0..1, binary(), timeout()) ::
:ok | {:error, :timeout} | {:error, :closed}
@callback send_eof(conn(), chan()) :: :ok | {:error, :closed}
@callback adjust_window(conn(), chan(), integer()) :: :ok
end

defmodule ErlangSshConnection do
@moduledoc false

@behaviour ErlangSshConnectionBehaviour

defdelegate session_channel(conn, initial_window_size, max_packet_size, timeout),
to: :ssh_connection

defdelegate subsystem(conn, chan, name, timeout), to: :ssh_connection
defdelegate close(conn, chan), to: :ssh_connection
defdelegate exec(conn, chan, command, timeout), to: :ssh_connection
defdelegate ptty_alloc(conn, chan, keyword, timeout), to: :ssh_connection
defdelegate send(conn, chan, type, data, timeout), to: :ssh_connection
defdelegate send_eof(conn, chan), to: :ssh_connection
defdelegate adjust_window(conn, chan, size), to: :ssh_connection
end

Mox.defmock(MockErlangSshConnection, for: ErlangSshConnectionBehaviour)

defmodule ErlangSshSftpBehaviour do
@moduledoc false

@type conn() :: term()
@type chan() :: pid()

# TODO
@callback start_channel(conn(), keyword()) :: {:ok, chan()} | {:error, term()}
end

defmodule ErlangSshSftp do
@moduledoc false

@behaviour ErlangSshSftpBehaviour

defdelegate start_channel(conn, options), to: :ssh_sftp
end

Mox.defmock(MockErlangSshSftp, for: ErlangSshSftpBehaviour)
Gen.defbehaviour(ErlangSshSftp.Behaviour, :ssh_sftp)
Gen.defdelegated(ErlangSshSftp, :ssh_sftp, behaviour: ErlangSshSftp.Behaviour)
Mox.defmock(MockErlangSshSftp, for: ErlangSshSftp.Behaviour)

0 comments on commit ef90bcb

Please sign in to comment.