$ git clone git@github.com:ruby/debug.git
$ bundle install
$ rake
- this will- Compile the C extension locally (which can also be done solely with
rake compile
). - Run tests.
- Re-generate
README.md
.
- Compile the C extension locally (which can also be done solely with
If you spot any problem, please open an issue.
$ rake test_all
$ rake test_console
$ rake test_protocol
$ ruby test/console/break_test.rb # run all tests in the specified file
$ ruby test/console/break_test.rb -h # to see all the test options
There is a test generator in debug.rb
project to make it easier to write tests.
This section shows you how to create test file by test generator. For more advanced information on creating tests, please take a look at gentest options. (You can also check by $bin/gentest -h
)
Let's say, we created target.rb
which is located in top level directory of debugger.
module Foo
class Bar
def self.a
"hello"
end
end
Bar.a
bar = Bar.new
end
$ bin/gentest target.rb
$ bin/gentest target.rb
DEBUGGER: Session start (pid: 11139)
[1, 9] in ~/workspace/debug/target.rb
=> 1| module Foo
2| class Bar
3| def self.a
4| "hello"
5| end
6| end
7| Bar.a
8| bar = Bar.new
9| end
=>#0 <main> at ~/workspace/debug/target.rb:1
INTERNAL_INFO: {"location":"~/workspace/debug/target.rb:1","line":1}
(rdbg)s
s
[1, 9] in ~/workspace/debug/target.rb
1| module Foo
=> 2| class Bar
3| def self.a
4| "hello"
5| end
6| end
7| Bar.a
8| bar = Bar.new
9| end
=>#0 <module:Foo> at ~/workspace/debug/target.rb:2
#1 <main> at ~/workspace/debug/target.rb:1
INTERNAL_INFO: {"location":"~/workspace/debug/target.rb:2","line":2}
(rdbg)n
n
[1, 9] in ~/workspace/debug/target.rb
1| module Foo
2| class Bar
=> 3| def self.a
4| "hello"
5| end
6| end
7| Bar.a
8| bar = Bar.new
9| end
=>#0 <class:Bar> at ~/workspace/debug/target.rb:3
#1 <module:Foo> at ~/workspace/debug/target.rb:2
# and 1 frames (use `bt' command for all frames)
INTERNAL_INFO: {"location":"~/workspace/debug/target.rb:3","line":3}
(rdbg)b 7
b 7
#0 BP - Line /Users/naotto/workspace/debug/target.rb:7 (line)
INTERNAL_INFO: {"location":"~/workspace/debug/target.rb:3","line":3}
(rdbg)c
c
[2, 9] in ~/workspace/debug/target.rb
2| class Bar
3| def self.a
4| "hello"
5| end
6| end
=> 7| Bar.a
8| bar = Bar.new
9| end
=>#0 <module:Foo> at ~/workspace/debug/target.rb:7
#1 <main> at ~/workspace/debug/target.rb:1
Stop by #0 BP - Line /Users/naotto/workspace/debug/target.rb:7 (line)
INTERNAL_INFO: {"location":"~/workspace/debug/target.rb:7","line":7}
(rdbg)q!
q!
created: /Users/naotto/workspace/debug/test/tool/../debug/foo_test.rb
class: FooTest
method: test_1629720194
If the file already exists, only method will be added to it.
# frozen_string_literal: true
require_relative '../support/console_test_case'
module DEBUGGER__
class FooTest < ConsoleTestCase
def program
<<~RUBY
1| module Foo
2| class Bar
3| def self.a
4| "hello"
5| end
6| end
7| Bar.a
8| bar = Bar.new
9| end
RUBY
end
def test_1629720194
debug_code(program) do
type 's'
assert_line_num 2
assert_line_text([
/\[1, 9\] in .*/,
/ 1\| module Foo/,
/=> 2\| class Bar/,
/ 3\| def self\.a/,
/ 4\| "hello"/,
/ 5\| end/,
/ 6\| end/,
/ 7\| Bar\.a/,
/ 8\| bar = Bar\.new/,
/ 9\| end/,
/=>\#0\t<module:Foo> at .*/,
/ \#1\t<main> at .*/
])
type 'n'
assert_line_num 3
assert_line_text([
/\[1, 9\] in .*/,
/ 1\| module Foo/,
/ 2\| class Bar/,
/=> 3\| def self\.a/,
/ 4\| "hello"/,
/ 5\| end/,
/ 6\| end/,
/ 7\| Bar\.a/,
/ 8\| bar = Bar\.new/,
/ 9\| end/,
/=>\#0\t<class:Bar> at .*/,
/ \#1\t<module:Foo> at .*/,
/ \# and 1 frames \(use `bt' command for all frames\)/
])
type 'b 7'
assert_line_text(/\#0 BP \- Line .*/)
type 'c'
assert_line_num 7
assert_line_text([
/\[2, 9\] in .*/,
/ 2\| class Bar/,
/ 3\| def self\.a/,
/ 4\| "hello"/,
/ 5\| end/,
/ 6\| end/,
/=> 7\| Bar\.a/,
/ 8\| bar = Bar\.new/,
/ 9\| end/,
/=>\#0\t<module:Foo> at .*/,
/ \#1\t<main> at .*/,
//,
/Stop by \#0 BP \- Line .*/
])
type 'q!'
end
end
end
end
You can get more information about gentest
here.
The default method name is test_#{some integer numbers}
, the class name is FooTest#{some integer numbers}
, and the file name will be foo_test.rb
.
The following table shows examples of the gentest options.
Command | Description | File | Class | Method |
---|---|---|---|---|
$ bin/gentest target.rb |
Run without any options | foo_test.rb |
FooTest... |
test_... |
$ bin/gentest target.rb --open=vscode |
Run the debugger with VScode | foo_test.rb |
FooTest... |
test_... |
$ bin/gentest target.rb -c step |
Specify the class name | step_test.rb |
StepTest... |
test_... |
$ bin/gentest target.rb -m test_step |
Specify the method name | foo_test.rb |
FooTest... |
test_step |
$ bin/gentest target.rb -c step -m test_step |
Specify the class name and the method name | step_test.rb |
StepTest... |
test_step |
- assert_line_num(expected)
Passes if expected
is equal to the location where debugger stops.
- assert_line_text(text)
Passes if text
is included in the last debugger log.
- assert_no_line_text(text)
Passes if text
is not included in the last debugger log.
- assert_debuggee_line_text(text)
Passes if text
is included in the debuggee log.
Currently, there are 2 kinds of test frameworks for DAP and CDP.
- Protocol-based tests
If you want to write protocol-based tests, you should use the test generator.
To run the test generator, you can enter $ bin/gentest target.rb --open=vscode
in the terminal, VSCode will be executed.
Also, if you enter $ bin/gentest target.rb --open=chrome
there, Chrome will be executed.
If you need to modify existing tests, it is basically a good idea to regenerate them by the test generator instead of rewriting them directly.
Please refer to the Microsoft "Debug Adapter Protocol" article to learn more about DAP formats.
Please refer to the "Chrome DevTools Protocol" official documentation to learn more about CDP formats.
- High-level tests
High-level tests are designed to test both DAP and CDP for a single method.
You can write tests as follows:
NOTE: Use req_terminate_debuggee
to finish debugging. You can't use any methods such as req_continue
, req_next
and so on.
require_relative '../support/test_case'
module DEBUGGER__
class BreakTest < TestCase
# PROGRAM is the target script.
PROGRAM = <<~RUBY
1| module Foo
2| class Bar
3| def self.a
4| "hello"
5| end
6| end
7| Bar.a
8| bar = Bar.new
9| end
RUBY
def test_break1
run_protocol_scenario PROGRAM do # Start debugging with DAP and CDP
req_add_breakpoint 5 # Set a breakpoint on line 5.
req_add_breakpoint 8 # Set a breakpoint on line 8.
req_continue # Resume the program.
assert_line_num 5 # Check if debugger stops at line 5.
req_continue # Resume the program.
assert_line_num 8 # Check if debugger stops at line 8.
req_terminate_debuggee # Terminate debugging.
end
end
end
end
- run_protocol_scenario program, dap: true, cdp: true, &scenario
Execute debugging program
with &scenario
. If you want to test it only for DAP, you can write as follows:
run_protocol_scenario program, cdp: false ...
- attach_to_dap_server(terminate_debuggee:)
Attach to the running DAP server through UNIX Domain Socket.
- attach_to_cdp_server
Attach to the running CDP server through TCP/IP.
- req_dap_disconnect
Disconnect from the currently connected DAP server.
- req_cdp_disconnect
Disconnect from the currently connected CDP server.
- req_add_breakpoint(lineno, path: temp_file_path, cond: nil)
Sends request to rdbg to add a breakpoint.
- req_delete_breakpoint bpnum
Sends request to rdbg to delete a breakpoint.
- req_set_exception_breakpoints(breakpoints)
Sends request to rdbg to set exception breakpoints. e.g.
req_set_exception_breakpoints([{ name: "RuntimeError", condition: "a == 1" }])
Please note that setExceptionBreakpoints
resets all exception breakpoints in every request.
So the following code will only set breakpoint for Exception
.
req_set_exception_breakpoints([{ name: "RuntimeError" }])
req_set_exception_breakpoints([{ name: "Exception" }])
This means you can also use
req_set_exception_breakpoints([])
to clear all exception breakpoints.
- req_continue
Sends request to rdbg to resume the program.
- req_step
Sends request to rdbg to step into next method.
- req_next
Sends request to rdbg to step over next method.
- req_finish
Sends request to rdbg to step out of current method.
- req_step_back
Sends request to rdbg to step back from current method.
- req_terminate_debuggee
Sends request to rdbg to terminate the debuggee.
- assert_hover_result(expected, expression)
Passes if result of expression
matches expected
.
expected
need to be a Hash object as follows:
assert_hover_result({value: '2', type: 'Integer'}, 'a')
NOTE: value
and type
need to be strings.
- assert_repl_result(expected, expression)
Passes if result of expression
matches expected
.
expected
need to be a Hash object as follows:
assert_repl_result({value: '2', type: 'Integer'}, 'a')
NOTE: value
and type
need to be strings.
- assert_watch_result(expected, expression)
Passes if result of expression
matches expected
.
expected
need to be a Hash object as follows:
assert_watch_result({value: '2', type: 'Integer'}, 'a')
NOTE: value
and type
need to be strings.
- assert_line_num(expected)
Passes if expected
is equal to the location where debugger stops.
- assert_locals_result(expected)
Passes if all of expected
local variable entries match the ones returned by debugger.
An variable entry looks like this: { name: "bar", value: "nil", type: "NilClass" }
.
Please note that both value
and type
need to be strings.
- assert_threads_result(expected)
Passes if both conditions are true:
- The number of expected patterns matches the number of threads.
- Every pattern matches a thread name. Notice that the order of threads info is not guaranteed.
Example:
assert_threads_result(
[
/\.rb:\d:in `<main>'/,
/\.rb:\d:in `block in foo'/
]
)
This project generates README.md
from the template misc/README.md.erb
So do not directly update README.md
. Instead, you should update the template's source and run
$ rake
to reflect the changes on README.md
.
- After updating
misc/README.md.erb
. - After updating
rdbg
executable's options. - After updating comments of debugger's commands.
You can manually test your changes with a simple Ruby script + a line of command. The following example will help you check:
- Breakpoint insertion.
- Resume from the breakpoint.
- Backtrace display.
- Information (local variables, ivars..etc.) display.
- Debugger exit.
# target.rb
class Foo
def first_call
second_call(20)
end
def second_call(num)
third_call_with_block do |ten|
forth_call(num, ten)
end
end
def third_call_with_block(&block)
@ivar1 = 10; @ivar2 = 20
yield(10)
end
def forth_call(num1, num2)
num1 + num2
end
end
Foo.new.first_call
$ exe/rdbg -e 'b 20;; c ;; bt ;; info ;; q!' -e c target.rb
❯ exe/rdbg -e 'b 20;; c ;; bt ;; info ;; q!' -e c target.rb
DEBUGGER: Session start (pid: 9815)
[1, 10] in target.rb
=> 1| class Foo
2| def first_call
3| second_call(20)
4| end
5|
6| def second_call(num)
7| third_call_with_block do |ten|
8| forth_call(num, ten)
9| end
10| end
=>#0 <main> at target.rb:1
(rdbg:commands) b 20
#0 BP - Line /PATH_TO_PROJECT/target.rb:20 (return)
(rdbg:commands) c
[15, 24] in target.rb
15| yield(10)
16| end
17|
18| def forth_call(num1, num2)
19| num1 + num2
=> 20| end
21| end
22|
23| Foo.new.first_call
24|
=>#0 Foo#forth_call(num1=20, num2=10) at target.rb:20 #=> 30
#1 block {|ten=10|} in second_call at target.rb:8
# and 4 frames (use `bt' command for all frames)
Stop by #0 BP - Line /PATH_TO_PROJECT/target.rb:20 (return)
(rdbg:commands) bt
=>#0 Foo#forth_call(num1=20, num2=10) at target.rb:20 #=> 30
#1 block {|ten=10|} in second_call at target.rb:8
#2 Foo#third_call_with_block(block=#<Proc:0x00007f9283101568 target.rb:7>) at target.rb:15
#3 Foo#second_call(num=20) at target.rb:7
#4 Foo#first_call at target.rb:3
#5 <main> at target.rb:23
(rdbg:commands) info
=>#0 Foo#forth_call(num1=20, num2=10) at target.rb:20 #=> 30
%self => #<Foo:0x00007f92831016d0 @ivar1=10, @ivar2=20>
%return => 30
num1 => 20
num2 => 10
@ivar1 => 10
@ivar2 => 20
(rdbg:commands) q!