Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -565,9 +565,21 @@ The `<...>` notation means the argument.
* Note that this feature is super slow.
* `catch <Error>`
* Set breakpoint on raising `<Error>`.
* `catch ... if: <expr>`
* stops only if `<expr>` is true as well.
* `catch ... pre: <command>`
* runs `<command>` before stopping.
* `catch ... do: <command>`
* stops and run `<command>`, and continue.
* `watch @ivar`
* Stop the execution when the result of current scope's `@ivar` is changed.
* Note that this feature is super slow.
* `watch ... if: <expr>`
* stops only if `<expr>` is true as well.
* `watch ... pre: <command>`
* runs `<command>` before stopping.
* `watch ... do: <command>`
* stops and run `<command>`, and continue.
* `del[ete]`
* delete all breakpoints.
* `del[ete] <bpnum>`
Expand Down
12 changes: 9 additions & 3 deletions lib/debug/breakpoint.rb
Original file line number Diff line number Diff line change
Expand Up @@ -328,12 +328,15 @@ def to_s
end

class WatchIVarBreakpoint < Breakpoint
def initialize ivar, object, current
def initialize ivar, object, current, cond: nil, command: nil
@ivar = ivar.to_sym
@object = object
@key = [:watch, @ivar].freeze
@key = [:watch, object.object_id, @ivar].freeze
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WatchBreakpoint could be initialized for multiple instances, so we should take that into consideration when filtering duplicated bps.


@current = current

@cond = cond
@command = command
super()
end

Expand All @@ -343,7 +346,10 @@ def watch_eval
begin
@prev = @current
@current = result
suspend

if @cond.nil? || @object.instance_eval(@cond)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO, the if condition of WatchBreakpoint should also be evaluated on the target object.

suspend
end
ensure
remove_instance_variable(:@prev)
end
Expand Down
22 changes: 21 additions & 1 deletion lib/debug/session.rb
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,12 @@ def process_command line

# * `catch <Error>`
# * Set breakpoint on raising `<Error>`.
# * `catch ... if: <expr>`
# * stops only if `<expr>` is true as well.
# * `catch ... pre: <command>`
# * runs `<command>` before stopping.
# * `catch ... do: <command>`
# * stops and run `<command>`, and continue.
when 'catch'
check_postmortem

Expand All @@ -570,11 +576,17 @@ def process_command line
# * `watch @ivar`
# * Stop the execution when the result of current scope's `@ivar` is changed.
# * Note that this feature is super slow.
# * `watch ... if: <expr>`
# * stops only if `<expr>` is true as well.
# * `watch ... pre: <command>`
# * runs `<command>` before stopping.
# * `watch ... do: <command>`
# * stops and run `<command>`, and continue.
when 'wat', 'watch'
check_postmortem

if arg && arg.match?(/\A@\w+/)
@tc << [:breakpoint, :watch, arg]
repl_add_watch_breakpoint(arg)
else
show_bps
return :retry
Expand Down Expand Up @@ -1286,6 +1298,14 @@ def repl_add_catch_breakpoint arg
add_bp bp
end

def repl_add_watch_breakpoint arg
expr = parse_break arg.strip
cond = expr[:if]
cmd = ['watch', expr[:pre], expr[:do]] if expr[:pre] || expr[:do]

@tc << [:breakpoint, :watch, expr[:sig], cond, cmd]
end

def add_catch_breakpoint pat
bp = CatchBreakpoint.new(pat)
add_bp bp
Expand Down
8 changes: 4 additions & 4 deletions lib/debug/thread_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -646,8 +646,8 @@ def make_breakpoint args

bp
when :watch
ivar, object, result = args[1..]
WatchIVarBreakpoint.new(ivar, object, result)
ivar, object, result, cond, command = args[1..]
WatchIVarBreakpoint.new(ivar, object, result, cond: cond, command: command)
else
raise "unknown breakpoint: #{args}"
end
Expand Down Expand Up @@ -890,7 +890,7 @@ def wait_next_action_
bp = make_breakpoint args
event! :result, :method_breakpoint, bp
when :watch
ivar = args[1]
ivar, cond, command = args[1..]
result = frame_eval(ivar)

if @success_last_eval
Expand All @@ -900,7 +900,7 @@ def wait_next_action_
else
current_frame.self
end
bp = make_breakpoint [:watch, ivar, object, result]
bp = make_breakpoint [:watch, ivar, object, result, cond, command]
event! :result, :watch_breakpoint, bp
else
event! :result, nil
Expand Down
63 changes: 59 additions & 4 deletions test/debug/watch_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def program
3|
4| def initialize(name)
5| @name = name
6| binding.break(do: "watch @name")
6| binding.b
7| end
8| end
9|
Expand All @@ -33,21 +33,76 @@ def program
def test_debugger_only_stops_when_the_ivar_of_instance_changes
debug_code(program) do
type 'continue'
# stops at binding.break
assert_line_text('Student#initialize(name="John")')
# stops when @name changes
type 'watch @name'
assert_line_text(/#0 BP - Watch #<Student:.*> @name = John/)
type 'continue'
assert_line_text(/Stop by #0 BP - Watch #<Student:.*> @name = John -> Josh/)
type 'continue'
end
end

def test_watch_command_isnt_repeatable
debug_code(program) do
type 'continue'
type 'watch @name'
type ''
assert_no_line_text(/duplicated breakpoint/)
type 'quit!'
end
end

def test_watch_works_with_command
debug_code(program) do
type 'continue'
type 'watch @name pre: p "1234"'
assert_line_text(/#0 BP - Watch #<Student:.*> @name = John/)
type 'continue'
assert_line_text(/1234/)
type 'continue'
end

debug_code(program) do
type 'continue'
type 'watch @name do: p "1234"'
assert_line_text(/#0 BP - Watch #<Student:.*> @name = John/)
type 'b 21'
type 'continue'
assert_line_text(/1234/)
type 'continue'
end
end

class ConditionTest < TestCase
def program
<<~RUBY
1| class Student
2| attr_accessor :name, :age
3|
4| def initialize(name, age)
5| @name = name
6| @age = age
7| binding.b(do: "watch @age if: name == 'Sean'")
8| end
9| end
10|
11| stan = Student.new("Stan", 30)
12| stan.age += 1
13| # only stops for Sean's age change
14| sean = Student.new("Sean", 25)
15| sean.age += 1
16|
17| a = 1 # additional line for line tp
RUBY
end

def test_condition_is_evaluated_in_the_watched_object
debug_code(program) do
type 'continue'
assert_line_text(/Stop by #\d BP - Watch #<Student:.*> @age = 25 -> 26/)
type 'continue'
assert_finish
end
end
end
end
end