summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJosé Valim <jose.valim@gmail.com>2018-11-17 17:13:11 +0100
committerGitHub <noreply@github.com>2018-11-17 17:13:11 +0100
commitd1e3e4dafd48e55898ec62235d6f91ab5aa7bc64 (patch)
treee1bb9a8e15623ec9f4ede109a18c0dd0656443cd
parente1ee8d134e74966dac59fd6deada1c97b92d0e71 (diff)
downloadelixir-d1e3e4dafd48e55898ec62235d6f91ab5aa7bc64.tar.gz
Introduce IEx.Server.run/1 (#8395)
Since now there may be multiple servers running, we also introduced IEx.Broker to negociate pry requests across multiple sessions. Shell sessions though are tracked separately from the remaining ones, as we delegate the shell control to Erlang's user_drv.
-rw-r--r--lib/iex/lib/iex.ex2
-rw-r--r--lib/iex/lib/iex/app.ex2
-rw-r--r--lib/iex/lib/iex/autocomplete.ex2
-rw-r--r--lib/iex/lib/iex/broker.ex181
-rw-r--r--lib/iex/lib/iex/evaluator.ex9
-rw-r--r--lib/iex/lib/iex/helpers.ex8
-rw-r--r--lib/iex/lib/iex/pry.ex6
-rw-r--r--lib/iex/lib/iex/server.ex155
-rw-r--r--lib/iex/test/iex/interaction_test.exs2
-rw-r--r--lib/iex/test/iex/server_test.exs168
-rw-r--r--lib/iex/test/test_helper.exs4
11 files changed, 390 insertions, 149 deletions
diff --git a/lib/iex/lib/iex.ex b/lib/iex/lib/iex.ex
index f648f2f1e..e0e1e82d3 100644
--- a/lib/iex/lib/iex.ex
+++ b/lib/iex/lib/iex.ex
@@ -773,7 +773,7 @@ defmodule IEx do
:ok = start_iex()
:ok = set_expand_fun()
:ok = run_after_spawn()
- IEx.Server.start(opts, mfa)
+ IEx.Server.run_from_shell(opts, mfa)
end)
end
diff --git a/lib/iex/lib/iex/app.ex b/lib/iex/lib/iex/app.ex
index 1c2f55adb..662626e53 100644
--- a/lib/iex/lib/iex/app.ex
+++ b/lib/iex/lib/iex/app.ex
@@ -4,7 +4,7 @@ defmodule IEx.App do
use Application
def start(_type, _args) do
- children = [IEx.Config, IEx.Pry]
+ children = [IEx.Config, IEx.Broker, IEx.Pry]
Supervisor.start_link(children, strategy: :one_for_one, name: IEx.Supervisor)
end
end
diff --git a/lib/iex/lib/iex/autocomplete.ex b/lib/iex/lib/iex/autocomplete.ex
index ca0972160..ec7620c9b 100644
--- a/lib/iex/lib/iex/autocomplete.ex
+++ b/lib/iex/lib/iex/autocomplete.ex
@@ -1,7 +1,7 @@
defmodule IEx.Autocomplete do
@moduledoc false
- def expand(expr, server \\ IEx.Server)
+ def expand(expr, server \\ IEx.Broker)
def expand('', server) do
expand_variable_or_import("", server)
diff --git a/lib/iex/lib/iex/broker.ex b/lib/iex/lib/iex/broker.ex
new file mode 100644
index 000000000..45b657f9f
--- /dev/null
+++ b/lib/iex/lib/iex/broker.ex
@@ -0,0 +1,181 @@
+defmodule IEx.Broker do
+ @moduledoc false
+ @name IEx.Broker
+
+ @type take_ref :: {takeover_ref :: reference(), server_ref :: reference()}
+
+ use GenServer
+
+ ## Shell API
+
+ @doc """
+ Finds the IEx server running inside `:user_drv`, on this node exclusively.
+ """
+ @spec shell :: pid | nil
+ def shell() do
+ # Locate top group leader, always registered as user
+ # can be implemented by group (normally) or user
+ # (if oldshell or noshell)
+ if user = Process.whereis(:user) do
+ case :group.interfaces(user) do
+ # Old or no shell
+ [] ->
+ case :user.interfaces(user) do
+ [] -> nil
+ [shell: shell] -> shell
+ end
+
+ # Get current group from user_drv
+ [user_drv: user_drv] ->
+ case :user_drv.interfaces(user_drv) do
+ [] -> nil
+ [current_group: group] -> :group.interfaces(group)[:shell]
+ end
+ end
+ end
+ end
+
+ @doc """
+ Finds the evaluator and server running inside `:user_drv`, on this node exclusively.
+ """
+ @spec evaluator :: {evaluator :: pid, server :: pid} | nil
+ def evaluator() do
+ if pid = shell() do
+ {:dictionary, dictionary} = Process.info(pid, :dictionary)
+ {dictionary[:evaluator], pid}
+ end
+ end
+
+ ## Broker API
+
+ def start_link(_) do
+ GenServer.start_link(__MODULE__, :ok, name: @name)
+ end
+
+ @doc """
+ Registers an IEx server in the broker.
+
+ All instances, except shell ones, are registered.
+ """
+ @spec register(pid) :: :ok
+ def register(pid) do
+ GenServer.call(@name, {:register, pid})
+ end
+
+ @doc """
+ Client responds to a takeover request.
+
+ The broker's PID is needed to support remote shells.
+ """
+ @spec respond(pid, take_ref, boolean()) :: :ok | {:error, :refused | :already_accepted}
+ def respond(broker_pid, take_ref, true) do
+ GenServer.call(broker_pid, {:accept, take_ref, Process.group_leader()})
+ end
+
+ def respond(broker_pid, take_ref, false) do
+ GenServer.call(broker_pid, {:refuse, take_ref})
+ end
+
+ @doc """
+ Client requests a takeover.
+ """
+ @spec take_over(binary, keyword) ::
+ {:ok, server :: pid, group_leader :: pid} | {:error, :no_iex | :refused}
+ def take_over(identifier, opts) do
+ GenServer.call(@name, {:take_over, identifier, opts}, :infinity)
+ end
+
+ ## Callbacks
+
+ @impl true
+ def init(:ok) do
+ state = %{
+ servers: %{},
+ takeovers: %{}
+ }
+
+ {:ok, state}
+ end
+
+ @impl true
+ def handle_call({:take_over, identifier, opts}, {_, ref} = from, state) do
+ case servers(state) do
+ [] ->
+ {:reply, {:error, :no_iex}, state}
+
+ servers ->
+ server_refs =
+ for {server_ref, server_pid} <- servers do
+ send(server_pid, {:take_over, self(), {ref, server_ref}, identifier, opts})
+ server_ref
+ end
+
+ state = put_in(state.takeovers[ref], {from, server_refs})
+ {:noreply, state}
+ end
+ end
+
+ def handle_call({:register, pid}, _from, state) do
+ ref = Process.monitor(pid)
+ state = put_in(state.servers[ref], pid)
+ {:reply, :ok, state}
+ end
+
+ def handle_call({:accept, {ref, _server_ref}, group_leader}, {server, _}, state) do
+ case pop_in(state.takeovers[ref]) do
+ {nil, state} ->
+ {:reply, {:error, :already_accepted}, state}
+
+ {{from, _}, state} ->
+ GenServer.reply(from, {:ok, server, group_leader})
+ {:reply, :ok, state}
+ end
+ end
+
+ def handle_call({:refuse, {ref, server_ref}}, _from, state) do
+ if takeover = state.takeovers[ref] do
+ {:reply, {:error, :refused}, refuse(state, ref, takeover, server_ref)}
+ else
+ {:reply, {:error, :refused}, state}
+ end
+ end
+
+ @impl true
+ def handle_info({:DOWN, server_ref, _, _, _}, state) do
+ {_pid, state} = pop_in(state.servers[server_ref])
+
+ state =
+ Enum.reduce(state.takeovers, state, fn {ref, takeover}, state ->
+ refuse(state, ref, takeover, server_ref)
+ end)
+
+ {:noreply, state}
+ end
+
+ defp refuse(state, ref, {from, server_refs}, server_ref) do
+ case List.delete(server_refs, server_ref) do
+ [] ->
+ {_, state} = pop_in(state.takeovers[ref])
+ GenServer.reply(from, {:error, :refused})
+ state
+
+ server_refs ->
+ put_in(state.takeovers[ref], {from, server_refs})
+ end
+ end
+
+ defp servers(state) do
+ if pid = local_or_remote_shell() do
+ [{Process.monitor(pid), pid} | Enum.to_list(state.servers)]
+ else
+ Enum.to_list(state.servers)
+ end
+ end
+
+ defp local_or_remote_shell() do
+ Enum.find_value([node() | Node.list()], fn node ->
+ server = :rpc.call(node, IEx.Broker, :shell, [])
+ if is_pid(server), do: server
+ end)
+ end
+end
diff --git a/lib/iex/lib/iex/evaluator.ex b/lib/iex/lib/iex/evaluator.ex
index 3c67c5360..543924826 100644
--- a/lib/iex/lib/iex/evaluator.ex
+++ b/lib/iex/lib/iex/evaluator.ex
@@ -14,6 +14,9 @@ defmodule IEx.Evaluator do
old_leader = Process.group_leader()
Process.group_leader(self(), leader)
+ old_server = Process.get(:iex_server)
+ Process.put(:iex_server, server)
+
evaluator = Process.get(:iex_evaluator)
Process.put(:iex_evaluator, command)
@@ -25,6 +28,12 @@ defmodule IEx.Evaluator do
after
Process.group_leader(self(), old_leader)
+ if old_server do
+ Process.put(:iex_server, old_server)
+ else
+ Process.delete(:iex_server)
+ end
+
cond do
is_nil(evaluator) ->
Process.delete(:iex_evaluator)
diff --git a/lib/iex/lib/iex/helpers.ex b/lib/iex/lib/iex/helpers.ex
index 421f3f4de..5829edcec 100644
--- a/lib/iex/lib/iex/helpers.ex
+++ b/lib/iex/lib/iex/helpers.ex
@@ -828,8 +828,8 @@ defmodule IEx.Helpers do
Respawns the current shell by starting a new shell process.
"""
def respawn do
- if whereis = IEx.Server.whereis() do
- send(whereis, {:respawn, self()})
+ if iex_server = Process.get(:iex_server) do
+ send(iex_server, {:respawn, self()})
end
dont_display_result()
@@ -852,8 +852,8 @@ defmodule IEx.Helpers do
"""
@doc since: "1.5.0"
def continue do
- if whereis = IEx.Server.whereis() do
- send(whereis, {:continue, self()})
+ if iex_server = Process.get(:iex_server) do
+ send(iex_server, {:continue, self()})
end
dont_display_result()
diff --git a/lib/iex/lib/iex/pry.ex b/lib/iex/lib/iex/pry.ex
index c38243565..81e6c58cb 100644
--- a/lib/iex/lib/iex/pry.ex
+++ b/lib/iex/lib/iex/pry.ex
@@ -69,9 +69,9 @@ defmodule IEx.Pry do
end
# We cannot use colors because IEx may be off
- case IEx.Server.take_over(request, opts) do
- :ok ->
- :ok
+ case IEx.Broker.take_over(request, [evaluator: self()] ++ opts) do
+ {:ok, server, group_leader} ->
+ IEx.Evaluator.init(:no_ack, server, group_leader, opts)
{:error, :no_iex} ->
extra =
diff --git a/lib/iex/lib/iex/server.ex b/lib/iex/lib/iex/server.ex
index 3fed39028..3feeb0c92 100644
--- a/lib/iex/lib/iex/server.ex
+++ b/lib/iex/lib/iex/server.ex
@@ -19,55 +19,6 @@ defmodule IEx.Server do
"""
@doc """
- Finds the IEx server running inside `:user_drv`, on this or another node.
- """
- @spec whereis :: pid | nil
- def whereis() do
- Enum.find_value([node() | Node.list()], fn node ->
- server = :rpc.call(node, IEx.Server, :local, [])
- if is_pid(server), do: server
- end)
- end
-
- @doc """
- Finds the IEx server running inside `:user_drv`, on this node exclusively.
- """
- @spec local :: pid | nil
- def local() do
- # Locate top group leader, always registered as user
- # can be implemented by group (normally) or user
- # (if oldshell or noshell)
- if user = Process.whereis(:user) do
- case :group.interfaces(user) do
- # Old or no shell
- [] ->
- case :user.interfaces(user) do
- [] -> nil
- [shell: shell] -> shell
- end
-
- # Get current group from user_drv
- [user_drv: user_drv] ->
- case :user_drv.interfaces(user_drv) do
- [] -> nil
- [current_group: group] -> :group.interfaces(group)[:shell]
- end
- end
- end
- end
-
- @doc """
- Finds the evaluator and server running inside `:user_drv`, on this node exclusively.
- """
- @spec evaluator :: {evaluator :: pid, server :: pid} | nil
- def evaluator() do
- if pid = IEx.Server.local() do
- {:dictionary, dictionary} = Process.info(pid, :dictionary)
- {dictionary[:evaluator], pid}
- end
- end
-
- @doc """
Starts a new IEx server session.
The accepted options are:
@@ -77,8 +28,13 @@ defmodule IEx.Server do
* `:binding` - an initial set of variables for the evaluator
"""
- @spec start(keyword) :: :ok
- def start(opts) when is_list(opts) do
+ @spec run(keyword) :: :ok
+ def run(opts) when is_list(opts) do
+ IEx.Broker.register(self())
+ run_without_registration(opts)
+ end
+
+ defp run_without_registration(opts) do
Process.flag(:trap_exit, true)
IO.puts(
@@ -91,60 +47,38 @@ defmodule IEx.Server do
## Private APIs
- # `start/1` with a callback. The server is spawned only after
- # the callback is done.
+ # Starts IEx to run directly from the Erlang shell.
+ #
+ # The server is spawned only after the callback is done.
#
# If there is any takeover during the callback execution
# we spawn a new server for it without waiting for its
# conclusion.
@doc false
- @spec start(keyword, {module, atom, [any]}) :: :ok
- def start(opts, {m, f, a}) do
+ @spec run_from_shell(keyword, {module, atom, [any]}) :: :ok
+ def run_from_shell(opts, {m, f, a}) do
Process.flag(:trap_exit, true)
{pid, ref} = spawn_monitor(m, f, a)
- start_loop(opts, pid, ref)
+ shell_loop(opts, pid, ref)
end
- defp start_loop(opts, pid, ref) do
+ defp shell_loop(opts, pid, ref) do
receive do
- {:take, take_pid, take_identifier, take_ref, take_opts} ->
- if allow_take?(take_identifier) do
- send(take_pid, {take_ref, Process.group_leader()})
- start(take_opts)
+ {:take_over, take_pid, take_ref, take_identifier, take_opts} ->
+ if take_over?(take_pid, take_ref, take_identifier) do
+ run_without_registration(take_opts)
else
- send(take_pid, {take_ref, nil})
- start_loop(opts, pid, ref)
+ shell_loop(opts, pid, ref)
end
{:DOWN, ^ref, :process, ^pid, :normal} ->
- start(opts)
+ run_without_registration(opts)
{:DOWN, ^ref, :process, ^pid, _reason} ->
:ok
end
end
- # Requests to take over the given shell from the current process.
- @doc false
- @spec take_over(binary, keyword) :: :ok | {:error, :no_iex} | {:error, :refused}
- def take_over(identifier, opts, server \\ whereis()) do
- if is_nil(server) do
- {:error, :no_iex}
- else
- ref = make_ref()
- opts = [evaluator: self()] ++ opts
- send(server, {:take, self(), identifier, ref, opts})
-
- receive do
- {^ref, nil} ->
- {:error, :refused}
-
- {^ref, leader} ->
- IEx.Evaluator.init(:no_ack, server, leader, opts)
- end
- end
- end
-
# Starts an evaluator using the provided options.
@doc false
@spec start_evaluator(keyword) :: pid
@@ -166,11 +100,11 @@ defmodule IEx.Server do
:ok
end
- defp restart(opts, evaluator, evaluator_ref, input) do
+ defp rerun(opts, evaluator, evaluator_ref, input) do
kill_input(input)
IO.puts("")
stop_evaluator(evaluator, evaluator_ref)
- start(opts)
+ run_without_registration(opts)
end
defp loop(state, evaluator, evaluator_ref) do
@@ -231,7 +165,7 @@ defmodule IEx.Server do
# A take process may also happen if the evaluator dies,
# then a new evaluator is created to replace the dead one.
defp handle_take_over(
- {:take, other, identifier, ref, opts},
+ {:take_over, take_pid, take_ref, take_identifier, opts},
state,
evaluator,
evaluator_ref,
@@ -240,16 +174,17 @@ defmodule IEx.Server do
) do
cond do
evaluator == opts[:evaluator] ->
- send(other, {ref, Process.group_leader()})
- kill_input(input)
- loop(iex_state(opts), evaluator, evaluator_ref)
+ if take_over?(take_pid, take_ref, true) do
+ kill_input(input)
+ loop(iex_state(opts), evaluator, evaluator_ref)
+ else
+ callback.(state)
+ end
- allow_take?(identifier) ->
- send(other, {ref, Process.group_leader()})
- restart(opts, evaluator, evaluator_ref, input)
+ take_over?(take_pid, take_ref, take_identifier) ->
+ rerun(opts, evaluator, evaluator_ref, input)
true ->
- send(other, {ref, nil})
callback.(state)
end
end
@@ -273,7 +208,7 @@ defmodule IEx.Server do
end
defp handle_take_over({:respawn, evaluator}, _state, evaluator, evaluator_ref, input, _callback) do
- restart([], evaluator, evaluator_ref, input)
+ rerun([], evaluator, evaluator_ref, input)
end
defp handle_take_over({:continue, evaluator}, state, evaluator, evaluator_ref, input, _callback) do
@@ -290,7 +225,7 @@ defmodule IEx.Server do
input,
_callback
) do
- restart([], evaluator, evaluator_ref, input)
+ rerun([], evaluator, evaluator_ref, input)
end
defp handle_take_over(
@@ -311,21 +246,35 @@ defmodule IEx.Server do
io_error("** (IEx.Error) #{type} when printing EXIT message: #{inspect(detail)}")
end
- restart([], evaluator, evaluator_ref, input)
+ rerun([], evaluator, evaluator_ref, input)
end
defp handle_take_over(_, state, _evaluator, _evaluator_ref, _input, callback) do
callback.(state)
end
- defp kill_input(nil), do: :ok
- defp kill_input(input), do: Process.exit(input, :kill)
+ defp take_over?(take_pid, take_ref, take_identifier) when is_binary(take_identifier) do
+ message = IEx.color(:eval_interrupt, "#{take_identifier}\nAllow? [Yn] ")
+ take_over?(take_pid, take_ref, yes?(IO.gets(:stdio, message)))
+ end
+
+ defp take_over?(take_pid, take_ref, take_response) do
+ case IEx.Broker.respond(take_pid, take_ref, take_response) do
+ :ok ->
+ true
+
+ {:error, :refused} ->
+ false
- defp allow_take?(identifier) do
- message = IEx.color(:eval_interrupt, "#{identifier}\nAllow? [Yn] ")
- yes?(IO.gets(:stdio, message))
+ {:error, :already_accepted} ->
+ io_error("** session was already accepted elsewhere")
+ false
+ end
end
+ defp kill_input(nil), do: :ok
+ defp kill_input(input), do: Process.exit(input, :kill)
+
defp yes?(string) do
is_binary(string) and String.trim(string) in ["", "y", "Y", "yes", "YES", "Yes"]
end
diff --git a/lib/iex/test/iex/interaction_test.exs b/lib/iex/test/iex/interaction_test.exs
index 591d1f53f..c53c0bd92 100644
--- a/lib/iex/test/iex/interaction_test.exs
+++ b/lib/iex/test/iex/interaction_test.exs
@@ -5,7 +5,7 @@ defmodule IEx.InteractionTest do
test "whole output" do
assert capture_io("IO.puts \"Hello world\"", fn ->
- IEx.Server.start([dot_iex_path: ""], {IEx, :dont_display_result, []})
+ IEx.Server.run(dot_iex_path: "")
end) =~
"Interactive Elixir (#{System.version()}) - press Ctrl+C to exit (type h() ENTER for help)" <>
"\niex(1)> Hello world\n:ok\niex(2)>"
diff --git a/lib/iex/test/iex/server_test.exs b/lib/iex/test/iex/server_test.exs
index 106edab7b..f387a6b30 100644
--- a/lib/iex/test/iex/server_test.exs
+++ b/lib/iex/test/iex/server_test.exs
@@ -3,65 +3,167 @@ Code.require_file("../test_helper.exs", __DIR__)
defmodule IEx.ServerTest do
use IEx.Case
+ require IEx
+
describe "options" do
test "prefix" do
assert capture_io(fn ->
- boot(prefix: "pry")
+ IEx.Server.run(prefix: "pry")
end) =~ "pry(1)> "
end
test "env" do
assert capture_io("__ENV__.file", fn ->
- boot(env: __ENV__)
+ IEx.Server.run(env: __ENV__)
end) =~ "server_test.exs"
end
end
- describe "take_over" do
- test "allows takeover of the shell during boot" do
- assert capture_io("Y\na+b", fn ->
- server = self()
+ describe "pry" do
+ test "no sessions" do
+ assert capture_io(fn ->
+ assert IEx.pry() == {:error, :no_iex}
+ end) =~ "Is an IEx shell running?"
+ end
- boot([], fn ->
- opts = [prefix: "dbg", binding: [a: 1, b: 2]]
- IEx.Server.take_over("iex:13", opts, server)
- end)
- end) =~ "dbg(1)> "
+ test "inside evaluator itself" do
+ assert capture_iex("require IEx; IEx.pry") =~ "Break reached"
end
- test "continues if takeover is refused" do
- assert capture_io("N\n", fn ->
- server = self()
+ test "outside of the evaluator with acceptance", config do
+ Process.register(self(), config.test)
- boot([], fn ->
- opts = [prefix: "dbg", binding: [a: 1, b: 2]]
- IEx.Server.take_over("iex:13", opts, server)
- end)
- end) =~ "iex(1)> "
+ {server, evaluator} = pry_session(config.test, "Y\niex_context")
+ client = pry_request([server])
+ send(evaluator, :run)
+
+ assert Task.await(server) =~ ":inside_pry"
+ assert Task.await(client) == :ok
end
- test "fails if callback during boot fails" do
- assert capture_io(fn ->
- boot([], fn -> exit(0) end)
- end) == ""
+ test "outside of the evaluator with refusal", config do
+ Process.register(self(), config.test)
+
+ {server, evaluator} = pry_session(config.test, "N\niex_context")
+ client = pry_request([server])
+ send(evaluator, :run)
+
+ assert Task.await(client) == {:error, :refused}
+ assert Task.await(server) =~ "undefined function iex_context"
end
- test "fails when there is no shell" do
- assert IEx.Server.take_over("iex:13", []) == {:error, :no_iex}
+ test "outside of the evaluator with crash", config do
+ Process.register(self(), config.test)
+
+ {server, _evaluator} = pry_session(config.test, "iex_context")
+ client = pry_request([server])
+
+ _ = Task.shutdown(server, :brutal_kill)
+ assert Task.await(client) == {:error, :refused}
end
- end
- test "pry wraps around takeover" do
- require IEx
+ test "outside of the evaluator with double acceptance", config do
+ Process.register(self(), config.test)
+
+ {server1, evaluator1} = pry_session(config.test, "Y\niex_context")
+ {server2, evaluator2} = pry_session(config.test, "Y\niex_context")
+ client = pry_request([server1, server2])
+
+ send(evaluator1, :run)
+ send(evaluator2, :run)
+ reply1 = Task.await(server1)
+ reply2 = Task.await(server2)
+
+ {accepted, refused} =
+ if reply1 =~ ":inside_pry", do: {reply1, reply2}, else: {reply2, reply1}
+
+ assert accepted =~ ":inside_pry"
+ assert refused =~ "** session was already accepted elsewhere"
+ assert refused =~ "undefined function iex_context"
+
+ assert Task.await(client) == :ok
+ end
+
+ test "outside of the evaluator with double refusal", config do
+ Process.register(self(), config.test)
+
+ {server1, evaluator1} = pry_session(config.test, "N\niex_context")
+ {server2, evaluator2} = pry_session(config.test, "N\niex_context")
+ client = pry_request([server1, server2])
+
+ send(evaluator1, :run)
+ send(evaluator2, :run)
+ reply1 = Task.await(server1)
+ reply2 = Task.await(server2)
- assert capture_io(fn ->
- assert IEx.pry() == {:error, :no_iex}
- end) =~ "Is an IEx shell running?"
+ assert reply1 =~ "undefined function iex_context"
+ assert reply2 =~ "undefined function iex_context"
+
+ assert Task.await(client) == {:error, :refused}
+ end
+
+ test "outside of the evaluator with acceptance and then refusal", config do
+ Process.register(self(), config.test)
+
+ {server1, evaluator1} = pry_session(config.test, "Y\niex_context")
+ {server2, evaluator2} = pry_session(config.test, "N\niex_context")
+ client = pry_request([server1, server2])
+
+ send(evaluator1, :run)
+ send(evaluator2, :run)
+ assert Task.await(server1) =~ ":inside_pry"
+ assert Task.await(server2) =~ "undefined function iex_context"
+
+ assert Task.await(client) == :ok
+ end
+
+ test "outside of the evaluator with refusal and then acceptance", config do
+ Process.register(self(), config.test)
+
+ {server1, evaluator1} = pry_session(config.test, "N\niex_context")
+ {server2, evaluator2} = pry_session(config.test, "Y\niex_context")
+ client = pry_request([server1, server2])
+
+ send(evaluator1, :run)
+ send(evaluator2, :run)
+ assert Task.await(server1) =~ "undefined function iex_context"
+ assert Task.await(server2) =~ ":inside_pry"
+
+ assert Task.await(client) == :ok
+ end
end
# Helpers
- defp boot(opts, callback \\ fn -> nil end) do
- IEx.Server.start(Keyword.merge([dot_iex_path: ""], opts), {:erlang, :apply, [callback, []]})
+ defp pry_session(name, session) do
+ task =
+ Task.async(fn ->
+ capture_iex("""
+ send(#{inspect(name)}, {:running, self()})
+ receive do: (:run -> :ok)
+ #{session}
+ """)
+ end)
+
+ assert_receive {:running, evaluator}
+ {task, evaluator}
+ end
+
+ defp pry_request(sessions) do
+ :erlang.trace(Process.whereis(IEx.Broker), true, [:receive, tracer: self()])
+ patterns = for %{pid: pid} <- sessions, do: {[:_, pid, :_], [], []}
+ :erlang.trace_pattern(:receive, patterns, [])
+
+ task =
+ Task.async(fn ->
+ iex_context = :inside_pry
+ IEx.pry()
+ end)
+
+ for _ <- sessions do
+ assert_receive {:trace, _, :receive, _}
+ end
+
+ task
end
end
diff --git a/lib/iex/test/test_helper.exs b/lib/iex/test/test_helper.exs
index 8cc7a407b..137bbd8a0 100644
--- a/lib/iex/test/test_helper.exs
+++ b/lib/iex/test/test_helper.exs
@@ -57,14 +57,14 @@ defmodule IEx.Case do
Options, if provided, will be set before the eval loop is started.
If you provide server options, it will be passed to
- IEx.Server.start to be used in the normal .iex loading process.
+ IEx.Server.run to be used in the normal .iex loading process.
"""
def capture_iex(input, options \\ [], server_options \\ [], capture_prompt \\ false) do
IEx.configure(options)
ExUnit.CaptureIO.capture_io([input: input, capture_prompt: capture_prompt], fn ->
server_options = Keyword.put_new(server_options, :dot_iex_path, "")
- IEx.Server.start(server_options, {IEx, :dont_display_result, []})
+ IEx.Server.run(server_options)
end)
|> strip_iex
end