diff --git a/lib/elixir/lib/macro/env.ex b/lib/elixir/lib/macro/env.ex index f3c1b831d37..b76b5e47110 100644 --- a/lib/elixir/lib/macro/env.ex +++ b/lib/elixir/lib/macro/env.ex @@ -44,8 +44,9 @@ defmodule Macro.Env do * `export_vars` - a list keeping all variables to be exported in a construct (may be `nil`) - * `match_vars` - a list of variables defined in a given match (is - `nil` when not inside a match) + * `match_vars` - controls how "new" variables are handled. Inside a + match it is a list with all variables in a match. Outside of a match + is either `:warn` or `:apply` * `prematch_vars` - a list of variables defined before a match (is `nil` when not inside a match) @@ -66,7 +67,7 @@ defmodule Macro.Env do @type local :: atom | nil @opaque export_vars :: vars | nil - @opaque match_vars :: vars | nil + @opaque match_vars :: vars | :warn | :apply @opaque prematch_vars :: vars | nil @type t :: %{__struct__: __MODULE__, @@ -103,7 +104,7 @@ defmodule Macro.Env do vars: [], lexical_tracker: nil, export_vars: nil, - match_vars: nil, + match_vars: :warn, prematch_vars: nil} end diff --git a/lib/elixir/src/elixir.erl b/lib/elixir/src/elixir.erl index 10c029fe054..a9066b7b881 100644 --- a/lib/elixir/src/elixir.erl +++ b/lib/elixir/src/elixir.erl @@ -177,8 +177,14 @@ env_for_eval(Env, Opts) -> false -> nil end, + FA = case lists:keyfind(function, 1, Opts) of + {function, {Function, Arity}} when is_atom(Function), is_integer(Arity) -> {Function, Arity}; + {function, nil} -> nil; + false -> nil + end, + Env#{ - file := File, module := Module, + file := File, module := Module, function := FA, macros := Macros, functions := Functions, requires := Requires, aliases := Aliases, line := Line }. diff --git a/lib/elixir/src/elixir_clauses.erl b/lib/elixir/src/elixir_clauses.erl index e93fd1c538d..4b31d4d5cfd 100644 --- a/lib/elixir/src/elixir_clauses.erl +++ b/lib/elixir/src/elixir_clauses.erl @@ -9,13 +9,13 @@ match(Fun, Expr, #{context := match} = E) -> Fun(Expr, E); -match(Fun, Expr, #{context := Context, match_vars := nil, prematch_vars := nil, vars := Vars} = E) -> +match(Fun, Expr, #{context := Context, match_vars := Match, prematch_vars := nil, vars := Vars} = E) -> {EExpr, EE} = Fun(Expr, E#{context := match, match_vars := [], prematch_vars := Vars}), - {EExpr, EE#{context := Context, match_vars := nil, prematch_vars := nil}}. + {EExpr, EE#{context := Context, match_vars := Match, prematch_vars := nil}}. -def({Meta, Args, Guards, Body}, E) -> +def({Meta, Args, Guards, Body}, #{match_vars := Match} = E) -> {EArgs, EA} = elixir_expand:expand(Args, E#{context := match, match_vars := []}), - {EGuards, EG} = guard(Guards, EA#{context := guard, match_vars := nil}), + {EGuards, EG} = guard(Guards, EA#{context := guard, match_vars := Match}), {EBody, _} = elixir_expand:expand(Body, EG#{context := ?key(E, context)}), {Meta, EArgs, EGuards, EBody}. diff --git a/lib/elixir/src/elixir_env.erl b/lib/elixir/src/elixir_env.erl index b634de38efb..25006680542 100644 --- a/lib/elixir/src/elixir_env.erl +++ b/lib/elixir/src/elixir_env.erl @@ -19,8 +19,8 @@ new() -> lexical_tracker => nil, %% holds the lexical tracker PID vars => [], %% a set of defined variables export_vars => nil, %% a set of variables to be exported in some constructs - match_vars => nil, %% a set of variables defined in the current match - prematch_vars => nil}. %% a set of variables defined before the current match + prematch_vars => nil, %% a set of variables defined before the current match + match_vars => warn}. %% handling of new variables linify({Line, Env}) -> Env#{line := Line}; diff --git a/lib/elixir/src/elixir_erl_try.erl b/lib/elixir/src/elixir_erl_try.erl index be829038a78..163f1fdee59 100644 --- a/lib/elixir/src/elixir_erl_try.erl +++ b/lib/elixir/src/elixir_erl_try.erl @@ -28,14 +28,14 @@ each_clause({'catch', Meta, Raw, Expr}, S) -> each_clause({rescue, Meta, [{in, _, [Left, Right]}], Expr}, S) -> {TempName, _, CS} = elixir_erl_var:build('_', S), - TempVar = {TempName, Meta, nil}, + TempVar = {TempName, Meta, 'Elixir'}, {Parts, Safe, FS} = rescue_guards(Meta, TempVar, Right, CS), Body = rescue_clause_body(Left, Expr, Safe, TempVar, Meta), build_rescue(Meta, Parts, Body, FS); each_clause({rescue, Meta, [{VarName, _, Context} = Left], Expr}, S) when is_atom(VarName), is_atom(Context) -> {TempName, _, CS} = elixir_erl_var:build('_', S), - TempVar = {TempName, Meta, nil}, + TempVar = {TempName, Meta, 'Elixir'}, Body = rescue_clause_body(Left, Expr, false, TempVar, Meta), build_rescue(Meta, [{TempVar, []}], Body, CS). @@ -76,7 +76,7 @@ rescue_guards(Meta, Var, Aliases, S) -> [] -> {[], S}; _ -> {VarName, _, CS} = elixir_erl_var:build('_', S), - StructVar = {VarName, Meta, nil}, + StructVar = {VarName, Meta, 'Elixir'}, Map = {'%{}', Meta, [{'__struct__', StructVar}, {'__exception__', true}]}, Match = {'=', Meta, [Map, Var]}, Guards = [{erl(Meta, '=='), Meta, [StructVar, Mod]} || Mod <- Elixir], diff --git a/lib/elixir/src/elixir_expand.erl b/lib/elixir/src/elixir_expand.erl index af703421c1c..5013c0f3fbf 100644 --- a/lib/elixir/src/elixir_expand.erl +++ b/lib/elixir/src/elixir_expand.erl @@ -357,10 +357,15 @@ expand({Name, Meta, Kind} = Var, #{vars := Vars} = E) when is_atom(Name), is_ato {var, true} -> form_error(Meta, ?key(E, file), ?MODULE, {undefined_var, Name, Kind}); _ -> - Message = - io_lib:format("variable \"~ts\" does not exist and is being expanded to \"~ts()\"," - " please use parentheses to remove the ambiguity or change the variable name", [Name, Name]), - elixir_errors:warn(?line(Meta), ?key(E, file), Message), + case ?key(E, match_vars) of + warn -> + Message = + io_lib:format("variable \"~ts\" does not exist and is being expanded to \"~ts()\"," + " please use parentheses to remove the ambiguity or change the variable name", [Name, Name]), + elixir_errors:warn(?line(Meta), ?key(E, file), Message); + apply -> + ok + end, expand({Name, Meta, []}, E) end end; diff --git a/lib/iex/lib/iex.ex b/lib/iex/lib/iex.ex index da6315a484d..0952fa36ada 100644 --- a/lib/iex/lib/iex.ex +++ b/lib/iex/lib/iex.ex @@ -2,10 +2,7 @@ defmodule IEx do @moduledoc ~S""" Elixir's interactive shell. - This module is the main entry point for Interactive Elixir and - in this documentation we will talk a bit about how IEx works. - - Notice that some of the functionalities described here will not be available + Some of the functionalities described here will not be available depending on your terminal. In particular, if you get a message saying that the smart terminal could not be run, some of the features described here won't work. @@ -47,12 +44,78 @@ defmodule IEx do set ERL_AFLAGS "-kernel shell_history enabled" + ## Expressions in IEx + + As an interactive shell, IEx evaluates expressions. This has some + interesting consequences that are worth discussing. + + The first one is that the code is truly evaluated and not compiled. + This means that any benchmarking done in the shell is going to have + skewed results. So never run any profiling nor benchmarks in the shell. + + Second, IEx allows you to break an expression into many lines, + since this is common in Elixir. For example: + + iex(1)> "ab + ...(1)> c" + "ab\nc" + + In the example above, the shell will be expecting more input until it + finds the closing quote. Sometimes it is not obvious which character + the shell is expecting, and the user may find themselves trapped in + the state of incomplete expression with no ability to terminate it other + than by exiting the shell. + + For such cases, there is a special break-trigger (`#iex:break`) that when + encountered on a line by itself will force the shell to break out of any + pending expression and return to its normal state: + + iex(1)> ["ab + ...(1)> c" + ...(1)> " + ...(1)> ] + ...(1)> #iex:break + ** (TokenMissingError) iex:1: incomplete expression + ## The Break command Inside IEx, hitting `Ctrl+C` will open up the `BREAK` menu. In this menu you can quit the shell, see process and ets tables information and much more. + ## Exiting the shell + + There are a few ways to quit the IEx shell: + + * via the `BREAK` menu (available via `Ctrl+C`) by typing `q`, pressing enter + * by hitting `Ctrl+C`, `Ctrl+C` + * by hitting `Ctrl+\` + + If you are connected to remote shell, it remains alive after disconnection. + + ## Prying and breakpoints + + IEx also has the ability to set breakpoints on Elixir code and + "pry" into running processes. This allows the developer to have + an IEx session run inside a given function. + + `IEx.pry/0` can be used when you are able to modify the source + code directly and recompile it: + + def my_fun(arg1, arg2) do + require IEx; IEx.pry + ... implementation ... + end + + When the code is executed, it will ask you for permission to be + introspected. + + Alternatively, you can use `IEx.break!/4` to setup a breakpoint + on a given module, function and arity you have no control of. + While `IEx.break!/4` is more flexible, it requires OTP 20+ and + it does not contain information about imports and aliases from + the source code. + ## The User Switch command Besides the break command, one can type `Ctrl+G` to get to the @@ -85,8 +148,16 @@ defmodule IEx do Since shells are isolated from each other, you can't access the variables defined in one shell from the other one. - The user switch command menu also allows developers to connect to remote - shells using the `r` command. A topic which we will discuss next. + The User Switch command can also be used to terminate an existing + session, for example when the evaluator gets stuck in an infinite + loop or when you are stuck typing an expression: + + User switch command + --> i + --> c + + The user switch command menu also allows developers to connect to + remote shells using the `r` command. A topic which we will discuss next. ## Remote shells @@ -209,48 +280,6 @@ defmodule IEx do iex(1)> [1, 2, 3, 4, 5] [1, 2, 3, ...] - ## Expressions in IEx - - As an interactive shell, IEx evaluates expressions. This has some - interesting consequences that are worth discussing. - - The first one is that the code is truly evaluated and not compiled. - This means that any benchmarking done in the shell is going to have - skewed results. So never run any profiling nor benchmarks in the shell. - - Second, IEx allows you to break an expression into many lines, - since this is common in Elixir. For example: - - iex(1)> "ab - ...(1)> c" - "ab\nc" - - In the example above, the shell will be expecting more input until it - finds the closing quote. Sometimes it is not obvious which character - the shell is expecting, and the user may find themselves trapped in - the state of incomplete expression with no ability to terminate it other - than by exiting the shell. - - For such cases, there is a special break-trigger (`#iex:break`) that when - encountered on a line by itself will force the shell to break out of any - pending expression and return to its normal state: - - iex(1)> ["ab - ...(1)> c" - ...(1)> " - ...(1)> ] - ...(1)> #iex:break - ** (TokenMissingError) iex:1: incomplete expression - - ## Exiting the shell - - There are a few ways to quit the IEx shell: - - * via the `BREAK` menu (available via `Ctrl+C`) by typing `q`, `Enter` - * by hitting `Ctrl+C`, `Ctrl+C` - * by hitting `Ctrl+\` - - If you are connected to remote shell, it remains alive after disconnection. """ @doc """ @@ -419,17 +448,23 @@ defmodule IEx do Pries into the process environment. This is useful for debugging a particular chunk of code - and inspect the state of a particular process. The process - is temporarily changed to trap exits (i.e. the process flag - `:trap_exit` is set to `true`) and has the `group_leader` changed - to support ANSI escape codes. Those values are reverted by - calling `respawn/0`, which starts a new IEx shell, freeing up - the pried one. - - When a process is pried, all code runs inside IEx and, as - such, it is evaluated and cannot access private functions - of the module being pried. Module functions still need to be - accessed via `Mod.fun(args)`. + when executed by a particular process. The process becomes + the evaluator of IEx commands and is temporarily changed to + have a custom group leader. Those values are reverted by + calling `IEx.Helpers.respawn/0`, which starts a new IEx shell, + freeing up the pried one. + + When a process is pried, all code runs inside IEx and has + access to all imports and aliases from the original code. + However, the code is evaluated and therefore cannot access + private functions of the module being pried. Module functions + still need to be accessed via `Mod.fun(args)`. + + Alternatively, you can use `IEx.break!/4` to setup a breakpoint + on a given module, function and arity you have no control of. + While `IEx.break!/4` is more flexible, it requires OTP 20+ and + it does not contain information about imports and aliases from + the source code. ## Examples @@ -470,7 +505,17 @@ defmodule IEx do Interactive Elixir - press Ctrl+C to exit (type h() ENTER for help) Setting variables or importing modules in IEx does not - affect the caller's environment (hence it is called `pry`). + affect the caller's environment. However, sending and + receiving messages will change the process state. + + ## Pry and mix test + + To use `IEx.pry/0` during tests, you need to run Mix inside + `iex` and pass the `--trace` to `mix test` to avoid running + into timeouts: + + iex -S mix test --trace + iex -S mix test path/to/file:line --trace """ defmacro pry() do quote do @@ -478,6 +523,131 @@ defmodule IEx do end end + @doc """ + Macro-based shortcut for `IEx.break!/4`. + """ + defmacro break!(ast, stops \\ 1) do + with {:/, _, [call, arity]} when is_integer(arity) <- ast, + {mod, fun, []} <- Macro.decompose_call(call) do + quote do + IEx.break!(unquote(mod), unquote(fun), unquote(arity), unquote(stops)) + end + else + _ -> + raise ArgumentError, "expected Mod.fun/arity, such as URI.parse/1, got: #{Macro.to_string(ast)}" + end + end + + @doc """ + Sets up a breakpoint in `module`, `function` and `arity` with + the given number of `stops`. + + This function will instrument the given module and load a new + version in memory with breakpoints at the given function and + arity. If the module is recompiled, all breakpoints are lost. + + When a breakpoint is reached, IEx will ask if you want to `pry` + the given function and arity. In other words, this works similar + to `IEx.pry/0` as the running process becomes the evaluator of + IEx commands and is temporarily changed to have a custom group + leader. However, differently from `IEx.pry/0`, aliases and imports + from the source code won't be available in the shell. + + IEx helpers includes many conveniences related to breakpoints. + Below they are listed with the full module, such as `IEx.Helpers.breaks/0`, + but remember it can be called directly as `breaks()` inside IEx. + They are: + + * `IEx.Helpers.break!/2` - sets up a breakpoint for a given `Mod.fun/arity` + * `IEx.Helpers.break!/4` - sets up a breakpoint for the given module, function, arity + * `IEx.Helpers.breaks/0` - prints all breakpoints and their ids + * `IEx.Helpers.continue/0` - continues until the next breakpoint in the same shell + * `IEx.Helpers.open/0` - opens editor on the current breakpoint + * `IEx.Helpers.remove_breaks/0` - removes all breakpoints in all modules + * `IEx.Helpers.remove_breaks/1` - removes all breakpoints in a given module + * `IEx.Helpers.reset_break/1` - sets the number of stops on the given id to zero + * `IEx.Helpers.reset_break/3` - sets the number of stops on the given module, function, arity to zero + * `IEx.Helpers.respawn/0` - starts a new shell (breakpoints will ask for permission once more) + * `IEx.Helpers.whereami/1` - shows the current location + + By default, the number of stops in a breakpoint is 1. Any follow-up + call won't stop the code execution unless another breakpoint is set. + + Alternatively, the number of be increased by passing the `stops` + argument. `IEx.Helpers.reset_break/1` and `IEx.Helpers.reset_break/3` + can be used to reset the number back to zero. Note the module remains + "instrumented" even after all stops on all breakpoints are consumed. + You can remove the instrumentation in a given module by calling + `IEx.Helpers.remove_breaks/1` and on all modules by calling + `IEx.Helpers.remove_breaks/0`. + + To exit a breakpoint, the developer can either invoke `continue()`, + which will block the shell until the next breakpoint is found or + the process terminates, or invoke `respawn()`, which starts a new IEx + shell, freeing up the pried one. + + This functionality only works on Elixir code and requires OTP 20+. + + ## Examples + + The following sets up a breakpoint on `URI.decode_query/2`: + + IEx.break!(URI, :decode_query, 2) + + The following call will setup a breakpoint that stops once. + To set a breakpoint that will stop 10 times: + + IEx.break!(URI, :decode_query, 10) + + `IEx.break!/2` is a convenience macro that allows breakpoints + to be given in the `Mod.fun/arity` format: + + require IEx + IEx.break!(URI.decode_query/2) + + Or to set a breakpoint that will stop 10 times: + + IEx.break!(URI.decode_query/2, 10) + + This function returns the breakpoint ID and will raise if there + is an error setting up the breakpoint. + + ## Breaks and mix test + + To use `IEx.break!/4` during tests, you need to run Mix inside + `iex` and pass the `--trace` to `mix test` to avoid running + into timeouts: + + iex -S mix test --trace + iex -S mix test path/to/file:line --trace + + """ + def break!(module, function, arity, stops \\ 10) do + case IEx.Pry.break(module, function, arity, stops) do + {:ok, id} -> + id + {:error, kind} -> + message = + case kind do + :missing_debug_info -> + "module #{inspect module} was not compiled with debug_info" + :no_beam_file -> + "could not find .beam file for #{inspect module}" + :non_elixir_module -> + "module #{inspect module} was not written in Elixir" + :otp_20_is_required -> + "you are running on an earlier OTP version than OTP 20" + :outdated_debug_info -> + "module #{inspect module} was not compiled with the latest debug_info" + :recompilation_failed -> + "the module could not be compiled with breakpoints (likely an internal error)" + :unknown_function_arity -> + "unknown function/macro #{Exception.format_mfa(module, function, arity)}" + end + raise "could not set breakpoint, " <> message + end + end + ## Callbacks # This is a callback invoked by Erlang shell utilities diff --git a/lib/iex/lib/iex/app.ex b/lib/iex/lib/iex/app.ex index 953cc21a3f5..1c2f55adbdd 100644 --- a/lib/iex/lib/iex/app.ex +++ b/lib/iex/lib/iex/app.ex @@ -4,18 +4,7 @@ defmodule IEx.App do use Application def start(_type, _args) do - tab = IEx.Config.new() - - case Supervisor.start_link([IEx.Config], strategy: :one_for_one, name: IEx.Supervisor) do - {:ok, pid} -> - {:ok, pid, tab} - {:error, _} = error -> - IEx.Config.delete(tab) - error - end - end - - def stop(tab) do - IEx.Config.delete(tab) + children = [IEx.Config, 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 afa5ce786e0..cd2f6e6815f 100644 --- a/lib/iex/lib/iex/autocomplete.ex +++ b/lib/iex/lib/iex/autocomplete.ex @@ -395,8 +395,8 @@ defmodule IEx.Autocomplete do ## Evaluator interface defp imports_from_env(server) do - with evaluator when is_pid(evaluator) <- server.evaluator(), - env_fields = IEx.Evaluator.fields_from_env(evaluator, [:functions, :macros]), + with {evaluator, server} <- server.evaluator(), + env_fields = IEx.Evaluator.fields_from_env(evaluator, server, [:functions, :macros]), %{functions: funs, macros: macros} <- env_fields do Enum.flat_map(funs ++ macros, &elem(&1, 1)) else @@ -405,8 +405,8 @@ defmodule IEx.Autocomplete do end defp aliases_from_env(server) do - with evaluator when is_pid(evaluator) <- server.evaluator, - %{aliases: aliases} <- IEx.Evaluator.fields_from_env(evaluator, [:aliases]) do + with {evaluator, server} <- server.evaluator(), + %{aliases: aliases} <- IEx.Evaluator.fields_from_env(evaluator, server, [:aliases]) do aliases else _ -> [] @@ -414,17 +414,17 @@ defmodule IEx.Autocomplete do end defp variables_from_binding(hint, server) do - with evaluator when is_pid(evaluator) <- server.evaluator() do - IEx.Evaluator.variables_from_binding(evaluator, hint) + with {evaluator, server} <- server.evaluator() do + IEx.Evaluator.variables_from_binding(evaluator, server, hint) else _ -> [] end end defp value_from_binding(ast_node, server) do - with evaluator when is_pid(evaluator) <- server.evaluator(), + with {evaluator, server} <- server.evaluator(), {var, map_key_path} <- extract_from_ast(ast_node, []) do - IEx.Evaluator.value_from_binding(evaluator, var, map_key_path) + IEx.Evaluator.value_from_binding(evaluator, server, var, map_key_path) else _ -> :error end diff --git a/lib/iex/lib/iex/config.ex b/lib/iex/lib/iex/config.ex index 810a8d24df1..9f08a1fea6e 100644 --- a/lib/iex/lib/iex/config.ex +++ b/lib/iex/lib/iex/config.ex @@ -119,17 +119,7 @@ defmodule IEx.Config do # Agent API def start_link(_) do - Agent.start_link(__MODULE__, :handle_init, [@table], [name: @agent]) - end - - def new() do - tab = :ets.new(@table, [:named_table, :public]) - true = :ets.insert_new(tab, [after_spawn: []]) - tab - end - - def delete(__MODULE__) do - :ets.delete(__MODULE__) + Agent.start_link(__MODULE__, :handle_init, [], name: @agent) end def after_spawn(fun) do @@ -146,9 +136,10 @@ defmodule IEx.Config do # Agent callbacks - def handle_init(tab) do - :public = :ets.info(tab, :protection) - tab + def handle_init do + :ets.new(@table, [:named_table, :public]) + true = :ets.insert_new(@table, [after_spawn: []]) + @table end def handle_after_spawn(tab, fun) do @@ -164,10 +155,9 @@ defmodule IEx.Config do end defp update_configuration(config) do - put = fn({key, value}) when key in @keys -> + Enum.each(config, fn {key, value} when key in @keys -> Application.put_env(:iex, key, value) - end - Enum.each(config, put) + end) end defp merge_option(:colors, old, new) when is_list(new), do: Keyword.merge(old, new) diff --git a/lib/iex/lib/iex/evaluator.ex b/lib/iex/lib/iex/evaluator.ex index c4a68e5c03d..6c11dbe1198 100644 --- a/lib/iex/lib/iex/evaluator.ex +++ b/lib/iex/lib/iex/evaluator.ex @@ -28,10 +28,10 @@ defmodule IEx.Evaluator do Gets a value out of the binding, using the provided variable name and map key path. """ - @spec value_from_binding(pid, atom, [atom]) :: {:ok, any} | :error - def value_from_binding(evaluator, var_name, map_key_path) do + @spec value_from_binding(pid, pid, atom, [atom]) :: {:ok, any} | :error + def value_from_binding(evaluator, server, var_name, map_key_path) do ref = make_ref() - send evaluator, {:value_from_binding, ref, self(), var_name, map_key_path} + send evaluator, {:value_from_binding, server, ref, self(), var_name, map_key_path} receive do {^ref, result} -> result @@ -44,10 +44,10 @@ defmodule IEx.Evaluator do Gets a list of variables out of the binding that match the passed variable prefix. """ - @spec variables_from_binding(pid, String.t) :: [String.t] - def variables_from_binding(evaluator, variable_prefix) do + @spec variables_from_binding(pid, pid, String.t) :: [String.t] + def variables_from_binding(evaluator, server, variable_prefix) do ref = make_ref() - send evaluator, {:variables_from_binding, ref, self(), variable_prefix} + send evaluator, {:variables_from_binding, server, ref, self(), variable_prefix} receive do {^ref, result} -> result @@ -59,10 +59,10 @@ defmodule IEx.Evaluator do @doc """ Returns the named fields from the current session environment. """ - @spec fields_from_env(pid, [atom]) :: %{optional(atom) => term} - def fields_from_env(evaluator, fields) do + @spec fields_from_env(pid, pid, [atom]) :: %{optional(atom) => term} + def fields_from_env(evaluator, server, fields) do ref = make_ref() - send evaluator, {:fields_from_env, ref, self(), fields} + send evaluator, {:fields_from_env, server, ref, self(), fields} receive do {^ref, result} -> result @@ -77,14 +77,14 @@ defmodule IEx.Evaluator do {result, state} = eval(code, iex_state, state) send server, {:evaled, self(), result} loop(state) - {:fields_from_env, ref, receiver, fields} -> + {:fields_from_env, ^server, ref, receiver, fields} -> send receiver, {ref, Map.take(state.env, fields)} loop(state) - {:value_from_binding, ref, receiver, var_name, map_key_path} -> + {:value_from_binding, ^server, ref, receiver, var_name, map_key_path} -> value = traverse_binding(state.binding, var_name, map_key_path) send receiver, {ref, value} loop(state) - {:variables_from_binding, ref, receiver, var_prefix} -> + {:variables_from_binding, ^server, ref, receiver, var_prefix} -> value = find_matched_variables(state.binding, var_prefix) send receiver, {ref, value} loop(state) @@ -111,17 +111,12 @@ defmodule IEx.Evaluator do end defp loop_state(server, history, opts) do - env = - if env = opts[:env] do - :elixir.env_for_eval(env, []) - else - :elixir.env_for_eval(file: "iex") - end - + env = opts[:env] || :elixir.env_for_eval(file: "iex") + env = %{env | match_vars: :apply} {_, _, env, scope} = :elixir.eval('import IEx.Helpers', [], env) binding = Keyword.get(opts, :binding, []) - state = %{binding: binding, scope: scope, env: env, server: server, history: history} + state = %{binding: binding, scope: scope, env: env, server: server, history: history} case opts[:dot_iex_path] do "" -> state @@ -200,15 +195,25 @@ defmodule IEx.Evaluator do code = iex_state.cache ++ latest_input line = iex_state.counter put_history(state) - handle_eval(Code.string_to_quoted(code, [line: line, file: "iex"]), code, line, iex_state, state) + put_whereami(state) + quoted = Code.string_to_quoted(code, line: line, file: "iex") + handle_eval(quoted, code, line, iex_state, state) after Process.delete(:iex_history) + Process.delete(:iex_whereami) end defp put_history(%{history: history}) do Process.put(:iex_history, history) end + defp put_whereami(%{env: %{file: "iex"}}) do + :ok + end + defp put_whereami(%{env: %{file: file, line: line}}) do + Process.put(:iex_whereami, {file, line}) + end + defp handle_eval({:ok, forms}, code, line, iex_state, state) do {result, binding, env, scope} = :elixir.eval_forms(forms, state.binding, state.env, state.scope) diff --git a/lib/iex/lib/iex/helpers.ex b/lib/iex/lib/iex/helpers.ex index 8194455e922..d79fba492ae 100644 --- a/lib/iex/lib/iex/helpers.ex +++ b/lib/iex/lib/iex/helpers.ex @@ -35,6 +35,7 @@ defmodule IEx.Helpers do * `i/1` - prints information about the given term * `ls/0` - lists the contents of the current directory * `ls/1` - lists the contents of the specified directory + * `open/1` - opens the source for the given module or function in your editor * `pid/1` - creates a PID from a string * `pid/3` - creates a PID with the 3 integer arguments passed * `pwd/0` - prints the current working directory @@ -54,6 +55,9 @@ defmodule IEx.Helpers do iex> e(IEx.Helpers) + This module also include helpers for debugging purposes, see + `IEx.break!/4` for more information. + To learn more about IEx as a whole, type `h(IEx)`. """ @@ -193,12 +197,58 @@ defmodule IEx.Helpers do dont_display_result() end + @doc """ + Opens the current prying location. + + This command only works inside a pry session started manually + via `IEx.pry/0` or a breakpoint set via `IEx.break!/4`. Calling + this function during a regular `IEx` session will print an error. + + Keep in mind the `open/0` location may not exist when prying + precompiled source code, such as Elixir itself. + + For more information and to open any module or function, see + `open/1`. + """ + def open() do + case Process.get(:iex_whereami) do + {file, line} -> + IEx.Introspection.open({file, line}) + _ -> + IO.puts IEx.color(:eval_error, "Pry session is not currently enabled") + end + + dont_display_result() + end + + @doc """ + Opens the given module, module/function/arity or file. + + This function uses the `ELIXIR_EDITOR` environment variable + and falls back to EDITOR if the former is not available. + + Since this function prints the result returned by the + editor, `ELIXIR_EDITOR` can be set "echo" if you prefer + to display the location rather than opening it. + + ## Examples + + iex> open MyApp + iex> open MyApp.fun/2 + iex> open "path/to/file" + + """ + defmacro open(term) do + quote do + IEx.Introspection.open(unquote(IEx.Introspection.decompose(term))) + end + end + @doc """ Prints the documentation for `IEx.Helpers`. """ def h() do IEx.Introspection.h(IEx.Helpers) - dont_display_result() end @doc """ @@ -626,14 +676,177 @@ defmodule IEx.Helpers do @doc """ Respawns the current shell by starting a new shell process. - - Returns `true` if it worked. """ def respawn do if whereis = IEx.Server.whereis do send whereis, {:respawn, self()} - dont_display_result() end + dont_display_result() + end + + @doc """ + Continues execution of the current process. + + This is usually called by sessions started with `IEx.pry/0` + or `IEx.break!/4`. This allows the current to execute until + the next breakpoint, which will automatically yield control + back to IEx without requesting permission to pry. + + If the running process terminates, a new IEx session is + started. + + While the process executes, the user will no longer have + control of the shell. If you would rather start a new shell, + use `respawn/0` instead. + """ + def continue do + if whereis = IEx.Server.whereis do + send whereis, {:continue, self()} + end + dont_display_result() + end + + @doc """ + Macro-based shortcut for `IEx.break!/4`. + """ + defmacro break!(ast, stops \\ 1) do + quote do + require IEx + IEx.break!(unquote(ast), unquote(stops)) + end + end + + @doc """ + Sets up a breakpoint in `module`, `function` and `arity` + with the given number of `stops`. + + See `IEx.break!/4` for a complete description of breakpoints + in IEx. + """ + defdelegate break!(module, function, arity, stops \\ 1), to: IEx + + @doc """ + Prints all breakpoints to the terminal. + """ + def breaks do + breaks(IEx.Pry.breaks()) + end + + defp breaks([]) do + IO.puts IEx.color(:eval_info, "No breakpoints set") + dont_display_result() + end + + defp breaks(breaks) do + entries = + for {id, module, {function, arity}, stops} <- breaks do + {Integer.to_string(id), + Exception.format_mfa(module, function, arity), + Integer.to_string(stops)} + end + + entries = [{"ID", "Module.function/arity", "Pending stops"} | entries] + + {id_max, mfa_max, stops_max} = + Enum.reduce(entries, {0, 0, 0}, fn {id, mfa, stops}, {id_max, mfa_max, stops_max} -> + {max(byte_size(id), id_max), + max(byte_size(mfa), mfa_max), + max(byte_size(stops), stops_max)} + end) + + [header | entries] = entries + + IO.puts "" + print_break(header, id_max, mfa_max) + IO.puts [String.duplicate("-", id_max + 2), ?\s, + String.duplicate("-", mfa_max + 2), ?\s, + String.duplicate("-", stops_max + 2)] + Enum.each(entries, &print_break(&1, id_max, mfa_max)) + IO.puts "" + + dont_display_result() + end + + defp print_break({id, mfa, stops}, id_max, mfa_max) do + IO.puts [?\s, String.pad_trailing(id, id_max + 2), + ?\s, String.pad_trailing(mfa, mfa_max + 2), + ?\s, stops] + end + + @doc """ + Sets the number of pending stops in the breakpoint + with the given id to zero. + + Returns `:ok` if there is such breakpoint id. `:not_found` + otherwise. + + Note the module remains "instrumented" on reset. If you would + like to effectively remove all breakpoints and instrumentation + code from a module, use `remove_breaks/1` instead. + """ + defdelegate reset_break(id), to: IEx.Pry + + @doc """ + Sets the number of pending stops in the given module, + function and arity to zero. + + If the module is not instrumented or if the given function + does not have a breakpoint, it is a no-op and it returns + `:not_found`. Otherwise it returns `:ok`. + + Note the module remains "instrumented" on reset. If you would + like to effectively remove all breakpoints and instrumentation + code from a module, use `remove_breaks/1` instead. + """ + defdelegate reset_break(module, function, arity), to: IEx.Pry + + @doc """ + Removes all breakpoints and instrumentation from `module`. + """ + defdelegate remove_breaks(module), to: IEx.Pry + + @doc """ + Removes all breakpoints and instrumentation from all modules. + """ + defdelegate remove_breaks(), to: IEx.Pry + + @doc """ + Prints the current location in a pry session. + + It expects a `radius` which chooses how many lines before and after + the current line we should print. By default the `radius` is of two + lines: + + Location: lib/iex/lib/iex/helpers.ex:79 + + 77: + 78: def recompile do + 79: require IEx; IEx.pry + 80: if mix_started?() do + 81: config = Mix.Project.config + + This command only works inside a pry session started manually + via `IEx.pry/0` or a breakpoint set via `IEx.break!/4`. Calling + this function during a regular `IEx` session will print an error. + + Keep in mind the `whereami/1` location may not exist when prying + precompiled source code, such as Elixir itself. + """ + def whereami(radius \\ 2) do + case Process.get(:iex_whereami) do + {file, line} -> + IO.puts IEx.color(:eval_info, ["Location: ", Path.relative_to_cwd(file), ":", Integer.to_string(line)]) + case IEx.Pry.whereami(file, line, radius) do + {:ok, lines} -> + IO.write [?\n, lines, ?\n] + :error -> + IO.puts IEx.color(:eval_error, "Could not extract source snippet. Location is not available.") + end + _ -> + IO.puts IEx.color(:eval_error, "Pry session is not currently enabled") + end + + dont_display_result() end @doc """ diff --git a/lib/iex/lib/iex/introspection.ex b/lib/iex/lib/iex/introspection.ex index 5cb47c4c3a0..df579fdf639 100644 --- a/lib/iex/lib/iex/introspection.ex +++ b/lib/iex/lib/iex/introspection.ex @@ -1,5 +1,5 @@ -# Convenience helpers for showing docs, specs and types -# from modules. Invoked directly from IEx.Helpers. +# Convenience helpers for showing docs, specs, types +# and opening modules. Invoked directly from IEx.Helpers. defmodule IEx.Introspection do @moduledoc false @@ -53,6 +53,108 @@ defmodule IEx.Introspection do end) end + @doc """ + Opens the given module, mfa, file/line, binary. + """ + def open(module) when is_atom(module) do + case open_mfa(module, :__info__, 1) do + {source, nil, _} -> open(source) + {_, tuple, _} -> open(tuple) + :error -> puts_error("Could not open: #{inspect module}. Module is not available.") + end + dont_display_result() + end + + def open({module, function}) when is_atom(module) and is_atom(function) do + case open_mfa(module, function, :*) do + {_, _, nil} -> puts_error("Could not open: #{inspect module}.#{function}. Function/macro is not available.") + {_, _, tuple} -> open(tuple) + :error -> puts_error("Could not open: #{inspect module}.#{function}. Module is not available.") + end + dont_display_result() + end + + def open({module, function, arity}) when is_atom(module) and is_atom(function) and is_integer(arity) do + case open_mfa(module, function, arity) do + {_, _, nil} -> puts_error("Could not open: #{inspect module}.#{function}/#{arity}. Function/macro is not available.") + {_, _, tuple} -> open(tuple) + :error -> puts_error("Could not open: #{inspect module}.#{function}/#{arity}. Module is not available.") + end + dont_display_result() + end + + def open({file, line}) when is_binary(file) and is_integer(line) do + if File.regular?(file) do + open("#{file}:#{line}") + else + puts_error("Could not open: #{inspect file}. File is not available.") + end + dont_display_result() + end + + def open(path) when is_binary(path) do + if editor = System.get_env("ELIXIR_EDITOR") || System.get_env("EDITOR") do + IO.write IEx.color(:eval_info, :os.cmd('#{editor} #{inspect path}')) + else + puts_error("Could not open: #{inspect path}. " <> + "Please set the ELIXIR_EDITOR or EDITOR environment variables with the " <> + "command line invocation of your favorite EDITOR.") + end + dont_display_result() + end + + def open(invalid) do + puts_error("Invalid arguments for open helper: #{inspect invalid}") + dont_display_result() + end + + defp open_mfa(module, fun, arity) do + with {:module, _} <- Code.ensure_loaded(module), + source when is_list(source) <- module.module_info(:compile)[:source] do + open_abstract_code(module, fun, arity, List.to_string(source)) + else + _ -> :error + end + end + + defp open_abstract_code(module, fun, arity, source) do + fun = Atom.to_string(fun) + + with beam when is_list(beam) <- :code.which(module), + {:ok, {_, [abstract_code: {:raw_abstract_v1, code}]}} <- :beam_lib.chunks(beam, [:abstract_code]) do + {_, module_pair, fa_pair} = + Enum.reduce(code, {source, nil, nil}, &open_abstract_code_reduce(&1, &2, fun, arity)) + {source, module_pair, fa_pair} + else + _ -> + {source, nil, nil} + end + end + + defp open_abstract_code_reduce(entry, {file, module_pair, fa_pair}, fun, arity) do + case entry do + {:attribute, _, :file, {ann_file, _}} -> + if Path.type(ann_file) == :absolute do + {List.to_string(ann_file), module_pair, fa_pair} + else + {file, module_pair, fa_pair} + end + {:attribute, ann, :module, _} -> + {file, {file, :erl_anno.line(ann)}, fa_pair} + {:function, ann, ann_fun, ann_arity, _} -> + case Atom.to_string(ann_fun) do + "MACRO-" <> ^fun when arity == :* or ann_arity == arity + 1 -> + {file, module_pair, fa_pair || {file, :erl_anno.line(ann)}} + ^fun when arity == :* or ann_arity == arity-> + {file, module_pair, fa_pair || {file, :erl_anno.line(ann)}} + _ -> + {file, module_pair, fa_pair} + end + _ -> + {file, module_pair, fa_pair} + end + end + @doc """ Prints documentation. """ diff --git a/lib/iex/lib/iex/pry.ex b/lib/iex/lib/iex/pry.ex index d9aa16e3d7a..d60fe9a4dfc 100644 --- a/lib/iex/lib/iex/pry.ex +++ b/lib/iex/lib/iex/pry.ex @@ -1,5 +1,16 @@ defmodule IEx.Pry do - @moduledoc false + @moduledoc """ + The low-level API for prying sessions and setting up breakpoints. + """ + + use GenServer + + @table __MODULE__ + @server __MODULE__ + @timeout :infinity + + @type id :: integer() + @type break :: {id, module, {function, arity}, pending :: non_neg_integer} @doc """ Callback for `IEx.pry/1`. @@ -8,37 +19,72 @@ defmodule IEx.Pry do `IEx.pry/1` as a macro. This function expects the binding (from `Kernel.binding/0`) and the environment (from `__ENV__/0`). """ - def pry(binding, env) do + def pry(binding, %Macro.Env{} = env) do + %{file: file, line: line, module: module, function: function_arity} = env + self = self() opts = [binding: binding, dot_iex_path: "", env: env, prefix: "pry"] - meta = "#{inspect self()} at #{Path.relative_to_cwd(env.file)}:#{env.line}" - desc = - case whereami(env.file, env.line, 2) do + + location = + case function_arity do + {function, arity} -> + "#{Exception.format_mfa(module, function, arity)} (#{Path.relative_to_cwd(file)}:#{line})" + _ -> + "#{Path.relative_to_cwd(file)}:#{line}" + end + + whereami = + case whereami(file, line, 2) do {:ok, lines} -> [?\n, ?\n, lines] - :error -> "" + :error -> [] end - res = IEx.Server.take_over("Request to pry #{meta}#{desc}", opts) + # If we are the current evaluator, it is because we just + # reached a pry/breakpoint and the user hit continue(). + # In both cases, we are safe to print and the request will + # suceed. + request = + case IEx.Server.evaluator do + {^self, _} -> + IO.puts IEx.color :eval_interrupt, "Break reached: #{location}#{whereami}" + "Prying #{inspect self} at #{location}" + _ -> + "Request to pry #{inspect self} at #{location}#{whereami}" + end - # We cannot use colors because IEx may be off. - case res do + # We cannot use colors because IEx may be off + case IEx.Server.take_over(request, opts) do + :ok -> + :ok {:error, :no_iex} -> extra = - case :os.type do - {:win32, _} -> " If you are Windows, you may need to start IEx with the --werl flag." - _ -> "" + if match?({:win32, _}, :os.type) do + " If you are using Windows, you may need to start IEx with the --werl flag." + else + "" end - IO.puts :stdio, "Cannot pry #{meta}. Is an IEx shell running?" <> extra - _ -> - :ok + IO.puts :stdio, "Cannot pry #{inspect self} at #{location}. Is an IEx shell running?" <> extra + {:error, :no_iex} + {:error, _} = error -> + error end + end - res + def pry(binding, opts) when is_list(opts) do + vars = for {k, _} when is_atom(k) <- binding, do: {k, nil} + pry(binding, %{:elixir.env_for_eval(opts) | vars: vars}) end @doc """ - Formats the location for whereami prying. + Formats the location for `whereami/3` prying. + + It receives the `file`, `line` and the snippet `radius` and + returns `{:ok, lines}`, where lines is a list of chardata + containing each formatted line, or `:error`. + + The actual line is especially formatted in bold. """ - def whereami(file, line, radius) do + def whereami(file, line, radius) + when is_binary(file) and is_integer(line) and is_integer(radius) and radius > 0 do with true <- File.regular?(file), [_ | _] = lines <- whereami_lines(file, line, radius) do {:ok, lines} @@ -66,4 +112,282 @@ defmodule IEx.Pry do [gutter, ": ", line_text] end end + + @doc """ + Sets up a breakpoint on the given module/function or + given module/function/arity. + """ + @spec break(module, function, arity, pos_integer) :: + {:ok, id} | + {:error, :recompilation_failed | :no_beam_file | :unknown_function_arity | + :otp_20_is_required | :missing_debug_info | :outdated_debug_info | + :non_elixir_module} + def break(module, function, arity, breaks \\ 1) + when is_atom(module) and is_atom(function) and is_integer(arity) and arity >= 0 and + is_integer(breaks) and breaks > 0 do + GenServer.call(@server, {:break, module, {function, arity}, breaks}, @timeout) + end + + @doc """ + Resets the breaks on a given breakpoint id. + """ + @spec reset_break(id) :: :ok | :not_found + def reset_break(id) when is_integer(id) do + GenServer.call(@server, {:reset_break, {id, :_, :_, :_}}, @timeout) + end + + @doc """ + Resets the breaks for the given module, function and arity. + + If the module is not instrumented or if the given function + does not have a breakpoint, it is a no-op and it returns + `:not_found`. Otherwise it returns `:ok`. + """ + @spec reset_break(module, function, arity) :: :ok | :not_found + def reset_break(module, function, arity) do + GenServer.call(@server, {:reset_break, {:_, module, {function, arity}, :_}}, @timeout) + end + + @doc """ + Removes all breakpoints on all modules. + + This effectively loads the non-instrumented version of + currently instrumented modules into memory. + """ + @spec remove_breaks :: :ok + def remove_breaks do + GenServer.call(@server, :remove_breaks, @timeout) + end + + @doc """ + Removes breakpoints in the given module. + + This effectively loads the non-instrumented version of + the module into memory. + """ + @spec remove_breaks(module) :: :ok | {:error, :no_beam_file} + def remove_breaks(module) do + GenServer.call(@server, {:remove_breaks, module}, @timeout) + end + + @doc """ + Returns all breakpoints. + """ + @spec breaks :: [break] + def breaks do + @server + |> GenServer.call(:breaks, @timeout) + |> Enum.sort() + end + + ## Callbacks + + @doc false + def start_link(_) do + GenServer.start_link(__MODULE__, :ok, name: @server) + end + + def init(:ok) do + Process.flag(:trap_exit, true) + :ets.new(@table, [:named_table, :public, write_concurrency: true]) + {:ok, 0} + end + + def handle_call({:break, module, fa, breaks}, _from, counter) do + # If there is a match for the given module and fa, and + # the module is still instrumented, we just increment + # the breaks counter. + # + # Otherwise we need to invoke the whole instrumentation + # tool chain. + case :ets.match_object(@table, {:_, module, fa, :_}) do + [{ref, module, fa, _}] -> + if instrumented?(module) do + :ets.insert(@table, {ref, module, fa, breaks}) + {:reply, {:ok, ref}, counter} + else + :ets.delete(@table, ref) + instrument_and_reply(module, fa, breaks, counter) + end + [] -> + instrument_and_reply(module, fa, breaks, counter) + end + end + + def handle_call({:reset_break, pattern}, _from, counter) do + reset = + for {ref, module, fa, _} <- :ets.match_object(@table, pattern) do + if instrumented?(module) do + :ets.insert(@table, {ref, module, fa, 0}) + true + else + :ets.delete(@table, ref) + false + end + end + + if Enum.any?(reset) do + {:reply, :ok, counter} + else + {:reply, :not_found, counter} + end + end + + def handle_call(:breaks, _from, counter) do + entries = + for {id, module, function_arity, breaks} <- :ets.tab2list(@table), + keep_instrumented(id, module) == :ok do + {id, module, function_arity, max(breaks, 0)} + end + {:reply, entries, counter} + end + + def handle_call(:remove_breaks, _from, _counter) do + # Make sure to deinstrument before clearing + # up the table to avoid race conditions. + @table + |> :ets.match({:_, :"$1", :_, :_}) + |> List.flatten + |> Enum.uniq + |> Enum.each(&deinstrument_if_instrumented/1) + true = :ets.delete_all_objects(@table) + {:reply, :ok, 0} + end + + def handle_call({:remove_breaks, module}, _from, counter) do + # Make sure to deinstrumented before clearing + # up the table to avoid race conditions. + reply = deinstrument_if_instrumented(module) + true = :ets.match_delete(@table, {:_, module, :_, :_}) + {:reply, reply, counter} + end + + defp keep_instrumented(id, module) do + if instrumented?(module) do + :ok + else + :ets.delete(@table, id) + :error + end + end + + defp deinstrument_if_instrumented(module) do + if instrumented?(module) do + deinstrument(module) + else + :ok + end + end + + defp deinstrument(module) do + with beam when is_list(beam) <- :code.which(module), + {:ok, binary} = File.read(beam) do + :code.purge(module) + {:module, _} = :code.load_binary(module, beam, binary) + :ok + else + _ -> {:error, :no_beam_file} + end + end + + defp instrument_and_reply(module, fa, breaks, counter) do + case fetch_elixir_debug_info_with_fa_check(module, fa) do + {:ok, beam, backend, elixir} -> + counter = counter + 1 + true = :ets.insert_new(@table, {counter, module, fa, breaks}) + entries = :ets.match_object(@table, {:_, module, :_, :_}) + {:reply, instrument(beam, backend, elixir, counter, entries), counter} + {:error, _} = error -> + {:reply, error, counter} + end + end + + defp fetch_elixir_debug_info_with_fa_check(module, fa) do + case :code.which(module) do + beam when is_list(beam) -> + case :beam_lib.chunks(beam, [:debug_info]) do + {:ok, {_, [debug_info: {:debug_info_v1, backend, {:elixir_v1, map, _} = elixir}]}} -> + case List.keyfind(map.definitions, fa, 0) do + {_, _, _, _} -> {:ok, beam, backend, elixir} + nil -> {:error, :unknown_function_arity} + end + {:ok, {_, [debug_info: {:debug_info_v1, _, _}]}} -> + {:error, :non_elixir_module} + {:error, :beam_lib, {:unknown_chunk, _, _}} -> + {:error, :otp_20_is_required} # TODO: Remove this when we require OTP 20+ + {:error, :beam_lib, {:missing_chunk, _, _}} -> + {:error, :missing_debug_info} + _ -> + {:error, :outdated_debug_info} + end + _ -> + {:error, :no_beam_file} + end + end + + defp instrument(beam, backend, {:elixir_v1, map, specs}, counter, entries) do + %{attributes: attributes, definitions: definitions, module: module} = map + map = %{map | attributes: [{:iex_pry, true} | attributes], + definitions: Enum.map(definitions, &instrument_definition(&1, map, entries))} + + with {:ok, forms} <- backend.debug_info(:erlang_v1, module, {:elixir_v1, map, specs}, []), + {:ok, _, binary, _} <- :compile.noenv_forms(forms, [:return | map.compile_opts]) do + :code.purge(module) + {:module, _} = :code.load_binary(module, beam, binary) + {:ok, counter} + else + _error -> + {:error, :recompilation_failed} + end + end + + defp instrument_definition({fa, kind, meta, clauses} = definition, map, entries) do + case List.keyfind(entries, fa, 2) do + {ref, _, ^fa, _} -> + %{module: module, file: file} = map + file = + case meta[:location] do + {file, _} -> file + _ -> file + end + opts = [module: module, file: file, function: fa] + clauses = Enum.map(clauses, &instrument_clause(&1, ref, opts)) + {fa, kind, meta, clauses} + nil -> + definition + end + end + + defp instrument_clause({meta, args, guards, clause}, ref, opts) do + opts = [line: Keyword.get(meta, :line, 1)] ++ opts + + # We store variables on a map ignoring the context. + # In the rare case where variables in different contexts + # have the same name, the last one wins. + {_, binding} = + Macro.prewalk(args, %{}, fn + {name, _, ctx} = var, acc when name != :_ and is_atom(name) and is_atom(ctx) -> + {var, Map.put(acc, name, var)} + expr, acc -> + {expr, acc} + end) + + # Generate the take_over condition with the ets lookup. + # Remember this is expanded AST, so no aliases allowed, + # no locals (such as the unary -) and so on. + condition = + quote do + # :ets.update_counter(table, key, {pos, inc, threshold, reset}) + case :ets.update_counter(unquote(@table), unquote(ref), {4, unquote(-1), unquote(-1), unquote(-1)}) do + unquote(-1) -> :ok + _ -> :"Elixir.IEx.Pry".pry(unquote(Map.to_list(binding)), unquote(opts)) + end + end + + {meta, args, guards, {:__block__, [], [condition, clause]}} + end + + defp instrumented?(module) do + module.module_info(:attributes)[:iex_pry] == [true] + end end diff --git a/lib/iex/lib/iex/server.ex b/lib/iex/lib/iex/server.ex index fd5a0101f0f..3c662da2775 100644 --- a/lib/iex/lib/iex/server.ex +++ b/lib/iex/lib/iex/server.ex @@ -47,13 +47,11 @@ defmodule IEx.Server do @doc """ Returns the PID of the IEx evaluator process if it exists. """ - @spec evaluator :: pid | nil + @spec evaluator :: {evaluator :: pid, server :: pid} | nil def evaluator() do - case IEx.Server.local do - nil -> nil - pid -> - {:dictionary, dictionary} = Process.info(pid, :dictionary) - dictionary[:evaluator] + if pid = IEx.Server.local do + {:dictionary, dictionary} = Process.info(pid, :dictionary) + {dictionary[:evaluator], pid} end end @@ -64,20 +62,19 @@ defmodule IEx.Server do @spec take_over(binary, keyword) :: :ok | {:error, :no_iex} | {:error, :refused} def take_over(identifier, opts, server \\ whereis()) do - cond do - is_nil(server) -> - {:error, :no_iex} - true -> - 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 + 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 @@ -130,11 +127,10 @@ defmodule IEx.Server do loop(iex_state(opts), evaluator, Process.monitor(evaluator)) end - defp rerun(opts, evaluator, evaluator_ref) do + defp rerun(opts, evaluator, evaluator_ref, input) do + kill_input(input) IO.puts("") - # We should not shutdown the evaluator if the - # rerun request came from the evaluator itself. - stop_evaluator(evaluator, evaluator_ref, evaluator != opts[:evaluator]) + stop_evaluator(evaluator, evaluator_ref) run(opts) end @@ -143,18 +139,16 @@ defmodule IEx.Server do """ @spec start_evaluator(keyword) :: pid def start_evaluator(opts) do - self_pid = self() - self_leader = Process.group_leader evaluator = opts[:evaluator] || - :proc_lib.start(IEx.Evaluator, :init, [:ack, self_pid, self_leader, opts]) + :proc_lib.start(IEx.Evaluator, :init, [:ack, self(), Process.group_leader, opts]) Process.put(:evaluator, evaluator) evaluator end - defp stop_evaluator(evaluator, evaluator_ref, done? \\ true) do + defp stop_evaluator(evaluator, evaluator_ref) do Process.delete(:evaluator) Process.demonitor(evaluator_ref, [:flush]) - done? && send(evaluator, {:done, self()}) + send(evaluator, {:done, self()}) :ok end @@ -180,7 +174,7 @@ defmodule IEx.Server do {:input, ^input, {:error, :terminated}} -> stop_evaluator(evaluator, evaluator_ref) msg -> - handle_take_over(msg, evaluator, evaluator_ref, input, fn -> + handle_take_over(msg, state, evaluator, evaluator_ref, input, fn state -> wait_input(state, evaluator, evaluator_ref, input) end) end @@ -190,17 +184,17 @@ defmodule IEx.Server do receive do {:evaled, ^evaluator, new_state} -> loop(new_state, evaluator, evaluator_ref) - {:EXIT, _pid, :interrupt} -> - # User did ^G while the evaluator was busy or stuck - io_error "** (EXIT) interrupted" - Process.delete(:evaluator) - Process.exit(evaluator, :kill) - Process.demonitor(evaluator_ref, [:flush]) - evaluator = start_evaluator([]) - loop(%{state | cache: ''}, evaluator, Process.monitor(evaluator)) msg -> - handle_take_over(msg, evaluator, evaluator_ref, nil, - fn -> wait_eval(state, evaluator, evaluator_ref) end) + handle_take_over(msg, state, evaluator, evaluator_ref, nil, + fn state -> wait_eval(state, evaluator, evaluator_ref) end) + end + end + + defp wait_take_over(state, evaluator, evaluator_ref) do + receive do + msg -> + handle_take_over(msg, state, evaluator, evaluator_ref, nil, + fn state -> wait_take_over(state, evaluator, evaluator_ref) end) end end @@ -208,37 +202,57 @@ 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}, evaluator, evaluator_ref, input, callback) do + defp handle_take_over({:take, other, identifier, ref, opts}, + state, evaluator, evaluator_ref, input, callback) do + cond do + evaluator == opts[:evaluator] -> + send other, {ref, Process.group_leader} + kill_input(input) + loop(iex_state(opts), evaluator, evaluator_ref) + allow_take?(identifier) -> + send other, {ref, Process.group_leader} + rerun(opts, evaluator, evaluator_ref, input) + true -> + send other, {ref, nil} + callback.(state) + end + end + + # User did ^G while the evaluator was busy or stuck + defp handle_take_over({:EXIT, _pid, :interrupt}, state, evaluator, evaluator_ref, input, _callback) do kill_input(input) + io_error "** (EXIT) interrupted" + Process.delete(:evaluator) + Process.exit(evaluator, :kill) + Process.demonitor(evaluator_ref, [:flush]) + evaluator = start_evaluator([]) + loop(%{state | cache: ''}, evaluator, Process.monitor(evaluator)) + end - if allow_take?(identifier) do - send other, {ref, Process.group_leader} - rerun(opts, evaluator, evaluator_ref) - else - send other, {ref, nil} - callback.() - end + defp handle_take_over({:respawn, evaluator}, _state, evaluator, evaluator_ref, input, _callback) do + rerun([], evaluator, evaluator_ref, input) end - defp handle_take_over({:respawn, evaluator}, evaluator, evaluator_ref, input, _callback) do + defp handle_take_over({:continue, evaluator}, state, evaluator, evaluator_ref, input, _callback) do kill_input(input) - rerun([], evaluator, evaluator_ref) + send(evaluator, {:done, self()}) + wait_take_over(state, evaluator, evaluator_ref) end defp handle_take_over({:DOWN, evaluator_ref, :process, evaluator, reason}, - evaluator, evaluator_ref, input, _callback) do + _state, evaluator, evaluator_ref, input, _callback) do try do - io_error Exception.format_banner({:EXIT, evaluator}, reason) + io_error "** (EXIT from #{inspect evaluator}) evaluator process exited with reason: " <> + Exception.format_exit(reason) catch type, detail -> io_error "** (IEx.Error) #{type} when printing EXIT message: #{inspect detail}" end - kill_input(input) - rerun([], evaluator, evaluator_ref) + rerun([], evaluator, evaluator_ref, input) end - defp handle_take_over(_, _evaluator, _evaluator_ref, _input, callback) do - callback.() + defp handle_take_over(_, state, _evaluator, _evaluator_ref, _input, callback) do + callback.(state) end defp kill_input(nil), do: :ok diff --git a/lib/iex/test/iex/autocomplete_test.exs b/lib/iex/test/iex/autocomplete_test.exs index 6522969780d..a5b73079539 100644 --- a/lib/iex/test/iex/autocomplete_test.exs +++ b/lib/iex/test/iex/autocomplete_test.exs @@ -11,13 +11,13 @@ defmodule IEx.AutocompleteTest do defmodule MyServer do def evaluator do - Process.get(:evaluator) + {Process.get(:evaluator), self()} end end defp eval(line) do ExUnit.CaptureIO.capture_io(fn -> - evaluator = MyServer.evaluator + {evaluator, _} = MyServer.evaluator Process.group_leader(evaluator, Process.group_leader) send evaluator, {:eval, self(), line <> "\n", %IEx.State{}} assert_receive {:evaled, _, _} diff --git a/lib/iex/test/iex/helpers_test.exs b/lib/iex/test/iex/helpers_test.exs index 40a7258a170..b068247f6c7 100644 --- a/lib/iex/test/iex/helpers_test.exs +++ b/lib/iex/test/iex/helpers_test.exs @@ -5,6 +5,224 @@ defmodule IEx.HelpersTest do import IEx.Helpers + describe "whereami" do + test "is disabled by default" do + assert capture_iex("whereami()") =~ + "Pry session is not currently enabled" + end + + test "shows current location for custom envs" do + whereami = capture_iex("whereami()", [], env: %{__ENV__ | line: 3}) + assert whereami =~ "test/iex/helpers_test.exs:3" + assert whereami =~ "3: defmodule IEx.HelpersTest do" + end + + test "prints message when location is not available" do + whereami = capture_iex("whereami()", [], env: %{__ENV__ | line: 30000}) + assert whereami =~ "test/iex/helpers_test.exs:30000" + assert whereami =~ "Could not extract source snippet. Location is not available." + + whereami = capture_iex("whereami()", [], env: %{__ENV__ | file: "nofile", line: 1}) + assert whereami =~ "nofile:1" + assert whereami =~ "Could not extract source snippet. Location is not available." + end + end + + if :erlang.system_info(:otp_release) >= '20' do + describe "breakpoints" do + setup do + on_exit fn -> IEx.Pry.remove_breaks() end + end + + test "sets up a breakpoint with macro syntax" do + assert break!(URI.decode_query/2) == 1 + assert IEx.Pry.breaks() == [{1, URI, {:decode_query, 2}, 1}] + end + + test "sets up a breakpoint on the given module" do + assert break!(URI, :decode_query, 2) == 1 + assert IEx.Pry.breaks() == [{1, URI, {:decode_query, 2}, 1}] + end + + test "resets breaks on the given id" do + assert break!(URI, :decode_query, 2) == 1 + assert reset_break(1) == :ok + assert IEx.Pry.breaks() == [{1, URI, {:decode_query, 2}, 0}] + end + + test "resets breaks on the given module" do + assert break!(URI, :decode_query, 2) == 1 + assert reset_break(URI, :decode_query, 2) == :ok + assert IEx.Pry.breaks() == [{1, URI, {:decode_query, 2}, 0}] + end + + test "removes breaks in the given module" do + assert break!(URI.decode_query/2) == 1 + assert remove_breaks(URI) == :ok + assert IEx.Pry.breaks() == [] + end + + test "removes breaks on all modules" do + assert break!(URI.decode_query/2) == 1 + assert remove_breaks() == :ok + assert IEx.Pry.breaks() == [] + end + + test "errors when setting up a break with no beam" do + assert_raise RuntimeError, + "could not set breakpoint, could not find .beam file for IEx.HelpersTest", + fn -> break!(__MODULE__, :setup, 1) end + end + + test "errors when setting up a break for unknown function" do + assert_raise RuntimeError, + "could not set breakpoint, unknown function/macro URI.unknown/2", + fn -> break!(URI, :unknown, 2) end + end + + test "errors for non elixir modules" do + assert_raise RuntimeError, + "could not set breakpoint, module :elixir was not written in Elixir", + fn -> break!(:elixir, :unknown, 2) end + end + + test "prints table with breaks" do + break!(URI, :decode_query, 2) + assert capture_io(fn -> breaks() end) == """ + + ID Module.function/arity Pending stops + ---- ----------------------- --------------- + 1 URI.decode_query/2 1 + + """ + + assert capture_io(fn -> URI.decode_query("foo=bar", %{}) end) != "" + assert capture_io(fn -> breaks() end) == """ + + ID Module.function/arity Pending stops + ---- ----------------------- --------------- + 1 URI.decode_query/2 0 + + """ + + assert capture_io(fn -> URI.decode_query("foo=bar", %{}) end) == "" + assert capture_io(fn -> breaks() end) == """ + + ID Module.function/arity Pending stops + ---- ----------------------- --------------- + 1 URI.decode_query/2 0 + + """ + end + + test "does not print table when there are no breaks" do + assert capture_io(fn -> breaks() end) == + "No breakpoints set\n" + end + end + end + + describe "open" do + @iex_helpers Path.expand("../../lib/iex/helpers.ex", __DIR__) + @elixir_erl Path.expand("../../../elixir/src/elixir.erl", __DIR__) + + test "opens Elixir module" do + assert capture_iex("open(IEx.Helpers)") |> maybe_trim_quotes() =~ + ~r/#{@iex_helpers}:1/ + end + + test "opens function" do + assert capture_iex("open(h)") |> maybe_trim_quotes() =~ + ~r/#{@iex_helpers}:\d+/ + end + + test "opens function/arity" do + assert capture_iex("open(b/1)") |> maybe_trim_quotes() =~ + ~r/#{@iex_helpers}:\d+/ + assert capture_iex("open(h/0)") |> maybe_trim_quotes() =~ + ~r/#{@iex_helpers}:\d+/ + end + + test "opens module.function" do + assert capture_iex("open(IEx.Helpers.b)") |> maybe_trim_quotes() =~ + ~r/#{@iex_helpers}:\d+/ + assert capture_iex("open(IEx.Helpers.h)") |> maybe_trim_quotes() =~ + ~r/#{@iex_helpers}:\d+/ + end + + test "opens module.function/arity" do + assert capture_iex("open(IEx.Helpers.b/1)") |> maybe_trim_quotes() =~ + ~r/#{@iex_helpers}:\d+/ + assert capture_iex("open(IEx.Helpers.h/0)") |> maybe_trim_quotes() =~ + ~r/#{@iex_helpers}:\d+/ + end + + test "opens Erlang module" do + assert capture_iex("open(:elixir)") |> maybe_trim_quotes() =~ + ~r/#{@elixir_erl}:\d+/ + end + + test "opens Erlang module.function" do + assert capture_iex("open(:elixir.start)") |> maybe_trim_quotes() =~ + ~r/#{@elixir_erl}:\d+/ + end + + test "opens Erlang module.function/arity" do + assert capture_iex("open(:elixir.start/2)") |> maybe_trim_quotes() =~ + ~r/#{@elixir_erl}:\d+/ + end + + test "errors if module is not available" do + assert capture_iex("open(:unknown)") == + "Could not open: :unknown. Module is not available." + end + + test "errors if module.function is not available" do + assert capture_iex("open(:unknown.unknown)") == + "Could not open: :unknown.unknown. Module is not available." + assert capture_iex("open(:elixir.unknown)") == + "Could not open: :elixir.unknown. Function/macro is not available." + end + + test "errors if module.function/arity is not available" do + assert capture_iex("open(:unknown.start/10)") == + "Could not open: :unknown.start/10. Module is not available." + assert capture_iex("open(:elixir.start/10)") == + "Could not open: :elixir.start/10. Function/macro is not available." + end + + test "opens the current pry location" do + assert capture_iex("open()", [], env: %{__ENV__ | line: 3}) |> maybe_trim_quotes() == + "#{__ENV__.file}:3" + end + + test "errors if prying is not available" do + assert capture_iex("open()") == "Pry session is not currently enabled" + end + + test "opens given {file, line}" do + assert capture_iex("open({#{inspect __ENV__.file}, 3})") |> maybe_trim_quotes() == + "#{__ENV__.file}:3" + end + + test "errors when given {file, line} is not available" do + assert capture_iex("open({~s[foo], 3})") == + "Could not open: \"foo\". File is not available." + end + + test "opens given path" do + assert capture_iex("open(#{inspect __ENV__.file})") |> maybe_trim_quotes() == + __ENV__.file + end + + defp maybe_trim_quotes(string) do + case :os.type do + {:win32, _} -> String.trim(string, "\"") + _ -> string + end + end + end + describe "clear" do test "clear the screen with ansi" do Application.put_env(:elixir, :ansi_enabled, true) @@ -532,7 +750,6 @@ defmodule IEx.HelpersTest do end describe "pid" do - test "builds a pid from string" do assert inspect(pid("0.32767.3276")) == "#PID<0.32767.3276>" assert inspect(pid("0.5.6")) == "#PID<0.5.6>" diff --git a/lib/iex/test/iex/interaction_test.exs b/lib/iex/test/iex/interaction_test.exs index 810a76f12a6..324dce27c27 100644 --- a/lib/iex/test/iex/interaction_test.exs +++ b/lib/iex/test/iex/interaction_test.exs @@ -102,11 +102,17 @@ defmodule IEx.InteractionTest do ~r/erl_eval/s end + test "exception while invoking conflicting helpers" do + import File, only: [open: 1], warn: false + assert capture_iex("open('README.md')", [], [env: __ENV__]) =~ + ~r"function open/1 imported from both File and IEx.Helpers" + end + test "receive exit" do assert capture_iex("spawn_link(fn -> exit(:bye) end); Process.sleep(1000)") =~ - ~r"\*\* \(EXIT from #PID<\d+\.\d+\.\d+>\) :bye" + ~r"\*\* \(EXIT from #PID<\d+\.\d+\.\d+>\) evaluator process exited with reason: :bye" assert capture_iex("spawn_link(fn -> exit({:bye, [:world]}) end); Process.sleep(1000)") =~ - ~r"\*\* \(EXIT from #PID<\d+\.\d+\.\d+>\) {:bye, \[:world\]}" + ~r"\*\* \(EXIT from #PID<\d+\.\d+\.\d+>\) evaluator process exited with reason: {:bye, \[:world\]}" end test "receive exit from exception" do @@ -115,7 +121,7 @@ defmodule IEx.InteractionTest do content = capture_iex("spawn_link(fn -> exit({%ArgumentError{}, [{:not_a_real_module, :function, 0, []}]}) end); Process.sleep(1000)") - assert content =~ ~r"\*\* \(EXIT from #PID<\d+\.\d+\.\d+>\) an exception was raised:\n" + assert content =~ ~r"\*\* \(EXIT from #PID<\d+\.\d+\.\d+>\) evaluator process exited with reason: an exception was raised:\n" assert content =~ ~r"\s{4}\*\* \(ArgumentError\) argument error\n" assert content =~ ~r"\s{8}:not_a_real_module\.function/0" end diff --git a/lib/iex/test/iex/pry_test.exs b/lib/iex/test/iex/pry_test.exs new file mode 100644 index 00000000000..d9e0534f4da --- /dev/null +++ b/lib/iex/test/iex/pry_test.exs @@ -0,0 +1,201 @@ +Code.require_file "../test_helper.exs", __DIR__ + +defmodule IEx.PryTest do + use ExUnit.Case + + setup do + on_exit fn -> IEx.Pry.remove_breaks() end + end + + describe "whereami" do + test "shows lines with radius" do + Application.put_env(:elixir, :ansi_enabled, true) + + {:ok, contents} = IEx.Pry.whereami(__ENV__.file, 3, 2) + assert IO.iodata_to_binary(contents) == """ + 1: Code.require_file "../test_helper.exs", __DIR__ + 2:\s + \e[1m 3: defmodule IEx.PryTest do + \e[22m 4: use ExUnit.Case + 5:\s + """ + + {:ok, contents} = IEx.Pry.whereami(__ENV__.file, 1, 4) + assert IO.iodata_to_binary(contents) == """ + \e[1m 1: Code.require_file "../test_helper.exs", __DIR__ + \e[22m 2:\s + 3: defmodule IEx.PryTest do + 4: use ExUnit.Case + 5:\s + """ + after + Application.delete_env(:elixir, :ansi_enabled) + end + + test "returns error for unknown files" do + assert IEx.Pry.whereami("unknown", 3, 2) == :error + end + + test "returns error for out of range lines" do + assert IEx.Pry.whereami(__ENV__.file, 1000, 2) == :error + end + end + + if :erlang.system_info(:otp_release) >= '20' do + describe "break" do + test "sets up a breakpoint on the given module" do + assert IEx.Pry.break(URI, :decode_query, 2) == {:ok, 1} + assert instrumented?(URI) + assert [_] = IEx.Pry.breaks() + end + + test "sets up multiple breakpoints in the same module" do + assert IEx.Pry.break(URI, :decode_query, 2) == {:ok, 1} + assert instrumented?(URI) + assert IEx.Pry.break(URI, :parse, 1) == {:ok, 2} + assert instrumented?(URI) + assert [_, _] = IEx.Pry.breaks() + end + + test "reinstruments if module has been reloaded" do + assert IEx.Pry.break(URI, :decode_query, 2) == {:ok, 1} + assert instrumented?(URI) + deinstrument!(URI) + refute instrumented?(URI) + assert IEx.Pry.break(URI, :parse, 1) == {:ok, 2} + assert instrumented?(URI) + assert [_, _] = IEx.Pry.breaks() + end + + test "returns id when breakpoint is already set" do + assert IEx.Pry.break(URI, :decode_query, 2) == {:ok, 1} + assert IEx.Pry.break(URI, :decode_query, 2) == {:ok, 1} + assert [_] = IEx.Pry.breaks() + end + + test "builds new id when breakpoint is already set on deinstrument" do + assert IEx.Pry.break(URI, :decode_query, 2) == {:ok, 1} + deinstrument!(URI) + assert IEx.Pry.break(URI, :decode_query, 2) == {:ok, 2} + assert [_] = IEx.Pry.breaks() + end + + test "errors when setting up a break with no beam" do + assert IEx.Pry.break(__MODULE__, :setup, 2) == {:error, :no_beam_file} + end + + test "errors when setting up a break for unknown function" do + assert IEx.Pry.break(URI, :unknown, 2) == {:error, :unknown_function_arity} + end + + test "errors for non elixir modules" do + assert IEx.Pry.break(:elixir, :unknown, 2) == {:error, :non_elixir_module} + end + end + + describe "breaks" do + test "returns all breaks" do + assert IEx.Pry.break(URI, :decode_query, 2) == {:ok, 1} + assert IEx.Pry.breaks() == + [{1, URI, {:decode_query, 2}, 1}] + + assert IEx.Pry.break(URI, :decode_query, 2, 10) == {:ok, 1} + assert IEx.Pry.breaks() == + [{1, URI, {:decode_query, 2}, 10}] + + assert IEx.Pry.break(URI, :parse, 1, 1) == {:ok, 2} + assert IEx.Pry.breaks() == + [{1, URI, {:decode_query, 2}, 10}, {2, URI, {:parse, 1}, 1}] + end + + test "sets negative break to 0" do + assert IEx.Pry.break(URI, :decode_query, 2) == {:ok, 1} + :ets.insert(IEx.Pry, {1, URI, {:decode_query, 2}, -1}) + assert IEx.Pry.breaks() == [{1, URI, {:decode_query, 2}, 0}] + end + + test "do not return break points for deinstrumented modules" do + assert IEx.Pry.break(URI, :parse, 1) == {:ok, 1} + assert IEx.Pry.breaks() == [{1, URI, {:parse, 1}, 1}] + deinstrument!(URI) + assert IEx.Pry.breaks() == [] + end + end + + describe "reset_break" do + test "resets break for given id" do + assert IEx.Pry.break(URI, :decode_query, 2) == {:ok, 1} + assert IEx.Pry.reset_break(1) == :ok + assert IEx.Pry.breaks() == [{1, URI, {:decode_query, 2}, 0}] + end + + test "resets break for given mfa" do + assert IEx.Pry.break(URI, :decode_query, 2) == {:ok, 1} + assert IEx.Pry.reset_break(URI, :decode_query, 2) == :ok + assert IEx.Pry.breaks() == [{1, URI, {:decode_query, 2}, 0}] + end + + test "returns not_found if module is deinstrumented" do + assert IEx.Pry.break(URI, :decode_query, 2) == {:ok, 1} + deinstrument!(URI) + assert IEx.Pry.reset_break(URI, :decode_query, 2) == :not_found + assert IEx.Pry.breaks() == [] + end + + test "returns not_found if mfa has no break" do + assert IEx.Pry.reset_break(URI, :decode_query, 2) == :not_found + end + + test "returns not_found if id is deinstrumented" do + assert IEx.Pry.break(URI, :decode_query, 2) == {:ok, 1} + deinstrument!(URI) + assert IEx.Pry.reset_break(1) == :not_found + assert IEx.Pry.breaks() == [] + end + + test "returns not_found if id has no break" do + assert IEx.Pry.reset_break(1) == :not_found + end + end + + describe "remove_breaks" do + test "removes all breaks" do + assert IEx.Pry.break(URI, :decode_query, 2) == {:ok, 1} + assert IEx.Pry.remove_breaks() == :ok + assert IEx.Pry.breaks() == [] + end + + test "removes all breaks even if module is deinstrumented" do + assert IEx.Pry.break(URI, :decode_query, 2) == {:ok, 1} + deinstrument!(URI) + assert IEx.Pry.remove_breaks() == :ok + assert IEx.Pry.breaks() == [] + end + + test "remove breaks in a given module" do + assert IEx.Pry.remove_breaks(Date.Range) == :ok + assert IEx.Pry.break(URI, :decode_query, 2) == {:ok, 1} + assert IEx.Pry.break(Date.Range, :__struct__, 1) == {:ok, 2} + assert IEx.Pry.remove_breaks(Date.Range) == :ok + assert IEx.Pry.breaks() == [{1, URI, {:decode_query, 2}, 1}] + end + + test "remove breaks in a given module even if deinstrumented" do + assert IEx.Pry.break(URI, :decode_query, 2) == {:ok, 1} + deinstrument!(URI) + assert IEx.Pry.breaks() == [] + end + end + end + + defp instrumented?(module) do + module.module_info(:attributes)[:iex_pry] == [true] + end + + defp deinstrument!(module) do + beam = :code.which(module) + :code.purge(module) + {:module, _} = :code.load_binary(module, beam, File.read!(beam)) + :ok + end +end diff --git a/lib/iex/test/test_helper.exs b/lib/iex/test/test_helper.exs index 4c1648f6c4a..9c5ded42f14 100644 --- a/lib/iex/test/test_helper.exs +++ b/lib/iex/test/test_helper.exs @@ -2,6 +2,8 @@ assert_timeout = String.to_integer( System.get_env("ELIXIR_ASSERT_TIMEOUT") || "500" ) +System.put_env("ELIXIR_EDITOR", "echo") + :ok = Application.start(:iex) IEx.configure([colors: [enabled: false]]) ExUnit.start [trace: "--trace" in System.argv, assert_receive_timeout: assert_timeout]