Skip to content

Commit ef90bcb

Browse files
committed
Generate code required to mock Erlang modules
1 parent 839c28c commit ef90bcb

File tree

4 files changed

+75
-78
lines changed

4 files changed

+75
-78
lines changed

lib/sshkit/connection.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ defmodule SSHKit.Connection do
55
A connection struct has the following fields:
66
77
* `host` - the name or IP of the remote host
8-
* `port` - the port to connect to
8+
* `port` - the port connected to
99
* `options` - additional connection options
1010
* `ref` - the underlying `:ssh` connection ref
1111
"""

test/support/functional_case.ex

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ defmodule SSHKit.FunctionalCase do
1717
@moduletag :functional
1818

1919
setup do
20-
# Stub mocks with implementations delegating to the proper Erlang modules
20+
# Stub mocks with implementations delegating to the original Erlang
21+
# modules, essentially "unmocking" them unless explicit expectations
22+
# are set up.
2123
Mox.stub_with(MockErlangSsh, ErlangSsh)
2224
Mox.stub_with(MockErlangSshConnection, ErlangSshConnection)
2325
Mox.stub_with(MockErlangSshSftp, ErlangSshSftp)

test/support/gen.ex

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
defmodule Gen do
2+
@moduledoc false
3+
4+
@doc """
5+
Generates a behaviour based on an existing module.
6+
7+
Mox requires a behaviour to be defined in order to create a mock. To mock
8+
core modules in tests - e.g. :ssh, :ssh_connection and :ssh_sftp - we need
9+
behaviours mirroring their public API.
10+
"""
11+
def defbehaviour(name, target) when is_atom(name) and is_atom(target) do
12+
info = moduledoc("Generated behaviour for #{inspect(target)}.")
13+
14+
body =
15+
for {fun, arity} <- functions(target) do
16+
args = 0..arity |> Enum.map(fn _ -> {:term, [], []} end) |> tl()
17+
18+
quote do
19+
@callback unquote(fun)(unquote_splicing(args)) :: term()
20+
end
21+
end
22+
23+
Module.create(name, info ++ body, Macro.Env.location(__ENV__))
24+
end
25+
26+
@doc """
27+
Generates a module delegating all function calls to another module.
28+
29+
Mox requires modules used for stubbing to implement the mocked behaviour. To
30+
mock core modules without behaviour definitions, we generate stand-in modules
31+
which delegate
32+
"""
33+
def defdelegated(name, target, options \\ [])
34+
when is_atom(name) and is_atom(target) and is_list(options) do
35+
info =
36+
moduledoc("Generated stand-in module for #{inspect(target)}.") ++
37+
behaviour(Keyword.get(options, :behaviour))
38+
39+
body =
40+
for {fun, arity} <- functions(target) do
41+
args = Macro.generate_arguments(arity, name)
42+
43+
quote do
44+
defdelegate unquote(fun)(unquote_splicing(args)), to: unquote(target)
45+
end
46+
end
47+
48+
Module.create(name, info ++ body, Macro.Env.location(__ENV__))
49+
end
50+
51+
defp functions(module) do
52+
exports = module.module_info(:exports)
53+
Keyword.drop(exports, ~w[__info__ module_info]a)
54+
end
55+
56+
defp moduledoc(nil), do: []
57+
defp moduledoc(docstr), do: [quote(do: @moduledoc(unquote(docstr)))]
58+
59+
defp behaviour(nil), do: []
60+
defp behaviour(name), do: [quote(do: @behaviour(unquote(name)))]
61+
end

test/support/mocks.ex

Lines changed: 10 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,13 @@
1-
defmodule ErlangSshBehaviour do
2-
@moduledoc false
1+
require Gen
32

4-
@type conn() :: term()
3+
Gen.defbehaviour(ErlangSsh.Behaviour, :ssh)
4+
Gen.defdelegated(ErlangSsh, :ssh, behaviour: ErlangSsh.Behaviour)
5+
Mox.defmock(MockErlangSsh, for: ErlangSsh.Behaviour)
56

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

10-
defmodule ErlangSsh do
11-
@moduledoc false
12-
13-
@behaviour ErlangSshBehaviour
14-
15-
defdelegate connect(host, port, options, timeout), to: :ssh
16-
defdelegate close(conn), to: :ssh
17-
end
18-
19-
Mox.defmock(MockErlangSsh, for: ErlangSshBehaviour)
20-
21-
defmodule ErlangSshConnectionBehaviour do
22-
@moduledoc false
23-
24-
@type conn() :: term()
25-
@type chan() :: integer()
26-
27-
@callback session_channel(conn(), integer(), integer(), timeout()) ::
28-
{:ok, chan()} | {:error, term()}
29-
@callback subsystem(conn(), chan(), charlist(), timeout()) ::
30-
:success | :failure | {:error, :timeout} | {:error, :closed}
31-
@callback close(conn(), chan()) :: :ok
32-
@callback exec(conn(), chan(), binary(), timeout()) ::
33-
:success | :failure | {:error, :timeout} | {:error, :closed}
34-
@callback ptty_alloc(conn(), chan(), keyword(), timeout()) ::
35-
:success | :failure | {:error, :timeout} | {:error, :closed}
36-
@callback send(conn(), chan(), 0..1, binary(), timeout()) ::
37-
:ok | {:error, :timeout} | {:error, :closed}
38-
@callback send_eof(conn(), chan()) :: :ok | {:error, :closed}
39-
@callback adjust_window(conn(), chan(), integer()) :: :ok
40-
end
41-
42-
defmodule ErlangSshConnection do
43-
@moduledoc false
44-
45-
@behaviour ErlangSshConnectionBehaviour
46-
47-
defdelegate session_channel(conn, initial_window_size, max_packet_size, timeout),
48-
to: :ssh_connection
49-
50-
defdelegate subsystem(conn, chan, name, timeout), to: :ssh_connection
51-
defdelegate close(conn, chan), to: :ssh_connection
52-
defdelegate exec(conn, chan, command, timeout), to: :ssh_connection
53-
defdelegate ptty_alloc(conn, chan, keyword, timeout), to: :ssh_connection
54-
defdelegate send(conn, chan, type, data, timeout), to: :ssh_connection
55-
defdelegate send_eof(conn, chan), to: :ssh_connection
56-
defdelegate adjust_window(conn, chan, size), to: :ssh_connection
57-
end
58-
59-
Mox.defmock(MockErlangSshConnection, for: ErlangSshConnectionBehaviour)
60-
61-
defmodule ErlangSshSftpBehaviour do
62-
@moduledoc false
63-
64-
@type conn() :: term()
65-
@type chan() :: pid()
66-
67-
# TODO
68-
@callback start_channel(conn(), keyword()) :: {:ok, chan()} | {:error, term()}
69-
end
70-
71-
defmodule ErlangSshSftp do
72-
@moduledoc false
73-
74-
@behaviour ErlangSshSftpBehaviour
75-
76-
defdelegate start_channel(conn, options), to: :ssh_sftp
77-
end
78-
79-
Mox.defmock(MockErlangSshSftp, for: ErlangSshSftpBehaviour)
11+
Gen.defbehaviour(ErlangSshSftp.Behaviour, :ssh_sftp)
12+
Gen.defdelegated(ErlangSshSftp, :ssh_sftp, behaviour: ErlangSshSftp.Behaviour)
13+
Mox.defmock(MockErlangSshSftp, for: ErlangSshSftp.Behaviour)

0 commit comments

Comments
 (0)