Skip to content

Commit

Permalink
make dbg breakpoints work when debugging in non interpreted mode
Browse files Browse the repository at this point in the history
  • Loading branch information
lukaszsamson committed Aug 27, 2023
1 parent 45d729f commit 63fb460
Show file tree
Hide file tree
Showing 3 changed files with 233 additions and 21 deletions.
51 changes: 39 additions & 12 deletions apps/elixir_ls_debugger/lib/debugger/server.ex
Original file line number Diff line number Diff line change
Expand Up @@ -159,19 +159,46 @@ defmodule ElixirLS.Debugger.Server do

{state, thread_id, _new_ids} = ensure_thread_id(state, pid, [])

# drop GenServer call to Debugger.Server from dbg callback
[_, first_frame | stacktrace] = Stacktrace.get(pid)

# overwrite erlang debugger bindings with exact elixir ones
first_frame = %{
first_frame
| bindings: Map.new(binding),
dbg_frame?: true,
dbg_env: Code.env_for_eval(env),
line: env.line
}
stacktrace =
case Stacktrace.get(pid) do
[_gen_server_frame, first_frame | stacktrace] ->
IO.puts("level #{first_frame.level}")
# drop GenServer call to Debugger.Server from dbg callback
# overwrite erlang debugger bindings with exact elixir ones
first_frame = %{
first_frame
| bindings: Map.new(binding),
dbg_frame?: true,
dbg_env: Code.env_for_eval(env),
module: env.module,
function: env.function,
file: env.file,
line: env.line
}

[first_frame | stacktrace]

[] ->
# no stacktrace if we are running in non interpreted mode
# build one frame stacktrace
# TODO look for ways of getting better stacktrace
first_frame = %Frame{
level: 1,
module: env.module,
function: env.function,
file: env.file,
args: [],
messages: [],
bindings: Map.new(binding),
dbg_frame?: true,
dbg_env: Code.env_for_eval(env),
line: env.line
}

[first_frame]
end

paused_process = %PausedProcess{stack: [first_frame | stacktrace], ref: ref}
paused_process = %PausedProcess{stack: stacktrace, ref: ref}
state = put_in(state.paused_processes[pid], paused_process)

body = %{"reason" => "breakpoint", "threadId" => thread_id, "allThreadsStopped" => false}
Expand Down
11 changes: 8 additions & 3 deletions apps/elixir_ls_debugger/lib/debugger/stacktrace.ex
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,13 @@ defmodule ElixirLS.Debugger.Stacktrace do
:dbg_env
]

def name(%__MODULE__{function: function} = frame) when not is_nil(function) do
{f, a} = frame.function
"#{inspect(frame.module)}.#{f}/#{a}"
end

def name(%__MODULE__{} = frame) do
"#{inspect(frame.module)}.#{frame.function}/#{Enum.count(frame.args)}"
"#{inspect(frame.module)}"
end
end

Expand All @@ -35,7 +40,7 @@ defmodule ElixirLS.Debugger.Stacktrace do
first_frame = %Frame{
level: level,
module: module,
function: function,
function: {function, Enum.count(args)},
args: args,
file: get_file(module),
# vscode raises invalid request when line is nil
Expand All @@ -57,7 +62,7 @@ defmodule ElixirLS.Debugger.Stacktrace do
%Frame{
level: level,
module: mod,
function: function,
function: {function, Enum.count(args)},
args: args,
file: get_file(mod),
# vscode raises invalid request when line is nil
Expand Down
192 changes: 186 additions & 6 deletions apps/elixir_ls_debugger/test/debugger_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2307,15 +2307,13 @@ defmodule ElixirLS.Debugger.ServerTest do
"task" => "run",
"taskArgs" => ["-e", "MixProject.Dbg.simple()"],
"projectDir" => File.cwd!()
# disable auto interpret
# "debugAutoInterpretAllModules" => false
})
)

assert_receive(response(_, 2, "launch", _), 3000)
assert_receive(event(_, "initialized", %{}), 5000)

# refute MixProject.Dbg in :int.interpreted()
assert MixProject.Dbg in :int.interpreted()

Server.receive_packet(server, request(5, "configurationDone", %{}))
assert_receive(response(_, 5, "configurationDone", %{}))
Expand Down Expand Up @@ -2491,15 +2489,13 @@ defmodule ElixirLS.Debugger.ServerTest do
"task" => "run",
"taskArgs" => ["-e", "MixProject.Dbg.pipe()"],
"projectDir" => File.cwd!()
# disable auto interpret
# "debugAutoInterpretAllModules" => false
})
)

assert_receive(response(_, 2, "launch", _), 3000)
assert_receive(event(_, "initialized", %{}), 5000)

# refute MixProject.Dbg in :int.interpreted()
assert MixProject.Dbg in :int.interpreted()

Server.receive_packet(server, request(5, "configurationDone", %{}))
assert_receive(response(_, 5, "configurationDone", %{}))
Expand Down Expand Up @@ -2640,6 +2636,190 @@ defmodule ElixirLS.Debugger.ServerTest do
})
end)
end

test "breaks on dbg when module is not interpreted", %{server: server} do
in_fixture(__DIR__, "mix_project", fn ->
abs_path = Path.absname("lib/dbg.ex")
Server.receive_packet(server, initialize_req(1, %{}))
assert_receive(response(_, 1, "initialize", _))

Server.receive_packet(
server,
launch_req(2, %{
"request" => "launch",
"type" => "mix_task",
"task" => "run",
"taskArgs" => ["-e", "MixProject.Dbg.simple()"],
"projectDir" => File.cwd!(),
# disable auto interpret
"debugAutoInterpretAllModules" => false
})
)

assert_receive(response(_, 2, "launch", _), 3000)
assert_receive(event(_, "initialized", %{}), 5000)

refute MixProject.Dbg in :int.interpreted()

Server.receive_packet(server, request(5, "configurationDone", %{}))
assert_receive(response(_, 5, "configurationDone", %{}))

Server.receive_packet(server, request(6, "threads", %{}))
assert_receive(response(_, 6, "threads", %{"threads" => threads}))

# ensure thread ids are unique
thread_ids = Enum.map(threads, & &1["id"])
assert Enum.count(Enum.uniq(thread_ids)) == Enum.count(thread_ids)

assert_receive event(_, "stopped", %{
"allThreadsStopped" => false,
"reason" => "breakpoint",
"threadId" => thread_id
}),
5_000

Server.receive_packet(server, stacktrace_req(7, thread_id))

assert_receive response(_, 7, "stackTrace", %{
"totalFrames" => 1,
"stackFrames" => [
%{
"column" => 0,
"id" => frame_id,
"line" => 5,
"name" => "MixProject.Dbg.simple/0",
"source" => %{"path" => ^abs_path}
}
]
})
when is_integer(frame_id)

Server.receive_packet(server, scopes_req(8, frame_id))

assert_receive response(_, 8, "scopes", %{
"scopes" => [
%{
"expensive" => false,
"indexedVariables" => 0,
"name" => "variables",
"namedVariables" => 1,
"variablesReference" => vars_id
},
%{
"expensive" => false,
"indexedVariables" => 0,
"name" => "process info",
"namedVariables" => _,
"variablesReference" => _
}
]
})

Server.receive_packet(server, vars_req(9, vars_id))

assert_receive response(_, 9, "variables", %{
"variables" => [
%{
"name" => "a",
"value" => "5",
"variablesReference" => 0
}
]
}),
1000

# stepIn is not supported
Server.receive_packet(server, step_in_req(12, thread_id))

assert_receive(
error_response(
_,
12,
"stepIn",
"notSupported",
"Kernel.dbg breakpoints do not support {command} command",
%{"command" => "stepIn"}
)
)

# stepOut is not supported
Server.receive_packet(server, step_out_req(13, thread_id))

assert_receive(
error_response(
_,
13,
"stepOut",
"notSupported",
"Kernel.dbg breakpoints do not support {command} command",
%{"command" => "stepOut"}
)
)

# next results in continue
Server.receive_packet(server, next_req(14, thread_id))
assert_receive response(_, 14, "next", %{})

assert_receive event(_, "stopped", %{
"allThreadsStopped" => false,
"reason" => "breakpoint",
"threadId" => ^thread_id
}),
5_000

Server.receive_packet(server, stacktrace_req(141, thread_id))

assert_receive response(_, 141, "stackTrace", %{
"totalFrames" => 1,
"stackFrames" => [
%{
"column" => 0,
"id" => frame_id,
"line" => 6,
"name" => "MixProject.Dbg.simple/0",
"source" => %{"path" => ^abs_path}
}
]
})
when is_integer(frame_id)

# continue
Server.receive_packet(server, continue_req(15, thread_id))
assert_receive response(_, 15, "continue", %{"allThreadsContinued" => true})

assert_receive event(_, "stopped", %{
"allThreadsStopped" => false,
"reason" => "breakpoint",
"threadId" => ^thread_id
}),
5_000

Server.receive_packet(server, stacktrace_req(151, thread_id))

assert_receive response(_, 151, "stackTrace", %{
"totalFrames" => 1,
"stackFrames" => [
%{
"column" => 0,
"id" => frame_id,
"line" => 7,
"name" => "MixProject.Dbg.simple/0",
"source" => %{"path" => ^abs_path}
}
]
})
when is_integer(frame_id)

Server.receive_packet(server, continue_req(16, thread_id))
assert_receive response(_, 16, "continue", %{"allThreadsContinued" => true})

refute_receive event(_, "thread", %{
"reason" => "exited",
"threadId" => ^thread_id
}),
1_000
end)
end
end
end

Expand Down

0 comments on commit 63fb460

Please sign in to comment.