From 03d76fb40a21bcef181335a5c0ebf75cf27bde43 Mon Sep 17 00:00:00 2001 From: st0012 Date: Mon, 15 Nov 2021 21:53:14 +0000 Subject: [PATCH] Add breakpoint section Add breakpoint examples Add breakpoint options Some corrections Introduce chained debugging commands --- guides/source/debugging_rails_applications.md | 281 ++++++++++++++++++ 1 file changed, 281 insertions(+) diff --git a/guides/source/debugging_rails_applications.md b/guides/source/debugging_rails_applications.md index efeb197da7e27..48db4b05116ea 100644 --- a/guides/source/debugging_rails_applications.md +++ b/guides/source/debugging_rails_applications.md @@ -468,6 +468,287 @@ instance variables: class variables: @@raise_on_missing_translations @@raise_on_open_redirects ``` +### Breakpoints + +There are many ways to insert and trigger a breakpoint in the debugger. In additional to adding debugging statements (e.g. `debugger`) directly in your code, you can also insert breakpoints with commands: + +- `break` (or `b`) + - `break` - list all breakpoints + - `break ` - set a breakpoint on the `num` line of the current file + - `break ` - set a breakpoint on the `num` line of `file` + - `break ` or `break ` - set a breakpoint on `Class#method` or `Class.method` + - `break .` - sets a breakpoint on `` result's `` method. +- `catch ` - set a breakpoint that'll stop when `Exception` is raised +- `watch <@ivar>` - set a breakpoint that'll stop when the result of current object's `@ivar` is changed (this is slow) + +And to remove them, you can use: + +- `delete` (or `del`) + - `delete` - delete all breakpoints + - `delete ` - delete the breakpoint with id `num` + +#### The break command + +**Set a breakpoint with specified line number - e.g. `b 28`** + +```rb +[20, 29] in ~/projects/rails-guide-example/app/controllers/posts_controller.rb + 20| end + 21| + 22| # POST /posts or /posts.json + 23| def create + 24| @post = Post.new(post_params) +=> 25| debugger + 26| + 27| respond_to do |format| + 28| if @post.save + 29| format.html { redirect_to @post, notice: "Post was successfully created." } +=>#0 PostsController#create at ~/projects/rails-guide-example/app/controllers/posts_controller.rb:25 + #1 ActionController::BasicImplicitRender#send_action(method="create", args=[]) at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/actionpack-7.0.0.alpha2/lib/action_controller/metal/basic_implicit_render.rb:6 + # and 72 frames (use `bt' command for all frames) +(rdbg) b 28 # break command +#0 BP - Line /Users/st0012/projects/rails-guide-example/app/controllers/posts_controller.rb:28 (line) +``` + +```rb +(rdbg) c # continue command +[23, 32] in ~/projects/rails-guide-example/app/controllers/posts_controller.rb + 23| def create + 24| @post = Post.new(post_params) + 25| debugger + 26| + 27| respond_to do |format| +=> 28| if @post.save + 29| format.html { redirect_to @post, notice: "Post was successfully created." } + 30| format.json { render :show, status: :created, location: @post } + 31| else + 32| format.html { render :new, status: :unprocessable_entity } +=>#0 block {|format=# 25| debugger + 26| + 27| respond_to do |format| + 28| if @post.save + 29| format.html { redirect_to @post, notice: "Post was successfully created." } +=>#0 PostsController#create at ~/projects/rails-guide-example/app/controllers/posts_controller.rb:25 + #1 ActionController::BasicImplicitRender#send_action(method="create", args=[]) at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/actionpack-7.0.0.alpha2/lib/action_controller/metal/basic_implicit_render.rb:6 + # and 72 frames (use `bt' command for all frames) +(rdbg) b @post.save # break command +#0 BP - Method @post.save at /Users/st0012/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/activerecord-7.0.0.alpha2/lib/active_record/suppressor.rb:43 + +``` + +```rb +(rdbg) c # continue command +[39, 48] in ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/activerecord-7.0.0.alpha2/lib/active_record/suppressor.rb + 39| SuppressorRegistry.suppressed[name] = previous_state + 40| end + 41| end + 42| + 43| def save(**) # :nodoc: +=> 44| SuppressorRegistry.suppressed[self.class.name] ? true : super + 45| end + 46| + 47| def save!(**) # :nodoc: + 48| SuppressorRegistry.suppressed[self.class.name] ? true : super +=>#0 ActiveRecord::Suppressor#save(#arg_rest=nil) at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/activerecord-7.0.0.alpha2/lib/active_record/suppressor.rb:44 + #1 block {|format=# 25| debugger + 26| + 27| respond_to do |format| + 28| if @post.save! + 29| format.html { redirect_to @post, notice: "Post was successfully created." } +=>#0 PostsController#create at ~/projects/rails-guide-example/app/controllers/posts_controller.rb:25 + #1 ActionController::BasicImplicitRender#send_action(method="create", args=[]) at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/actionpack-7.0.0.alpha2/lib/action_controller/metal/basic_implicit_render.rb:6 + # and 72 frames (use `bt' command for all frames) +(rdbg) catch ActiveRecord::RecordInvalid # command +#1 BP - Catch "ActiveRecord::RecordInvalid" +``` + +```rb +(rdbg) c # continue command +[75, 84] in ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/activerecord-7.0.0.alpha2/lib/active_record/validations.rb + 75| def default_validation_context + 76| new_record? ? :create : :update + 77| end + 78| + 79| def raise_validation_error +=> 80| raise(RecordInvalid.new(self)) + 81| end + 82| + 83| def perform_validations(options = {}) + 84| options[:validate] == false || valid?(options[:context]) +=>#0 ActiveRecord::Validations#raise_validation_error at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/activerecord-7.0.0.alpha2/lib/active_record/validations.rb:80 + #1 ActiveRecord::Validations#save!(options={}) at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/activerecord-7.0.0.alpha2/lib/active_record/validations.rb:53 + # and 88 frames (use `bt' command for all frames) + +Stop by #1 BP - Catch "ActiveRecord::RecordInvalid" +``` + +#### The watch command + +**Stop when the instance variable is changed - e.g. `watch @_response_body`** + +```rb +[20, 29] in ~/projects/rails-guide-example/app/controllers/posts_controller.rb + 20| end + 21| + 22| # POST /posts or /posts.json + 23| def create + 24| @post = Post.new(post_params) +=> 25| debugger + 26| + 27| respond_to do |format| + 28| if @post.save! + 29| format.html { redirect_to @post, notice: "Post was successfully created." } +=>#0 PostsController#create at ~/projects/rails-guide-example/app/controllers/posts_controller.rb:25 + #1 ActionController::BasicImplicitRender#send_action(method="create", args=[]) at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/actionpack-7.0.0.alpha2/lib/action_controller/metal/basic_implicit_render.rb:6 + # and 72 frames (use `bt' command for all frames) +(rdbg) watch @_response_body # command +#0 BP - Watch # @_response_body = +``` + +```rb +(rdbg) c # continue command +[173, 182] in ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/actionpack-7.0.0.alpha2/lib/action_controller/metal.rb + 173| body = [body] unless body.nil? || body.respond_to?(:each) + 174| response.reset_body! + 175| return unless body + 176| response.body = body + 177| super +=> 178| end + 179| + 180| # Tests if render or redirect has already happened. + 181| def performed? + 182| response_body || response.committed? +=>#0 ActionController::Metal#response_body=(body=["You are being ["You are being false}) at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/actionpack-7.0.0.alpha2/lib/action_controller/metal/redirecting.rb:74 + # and 82 frames (use `bt' command for all frames) + +Stop by #0 BP - Watch # @_response_body = -> ["You are being redirected."] +(rdbg) +``` + +#### Breakpoint options + +In addition to different types of breakpoints, you can also specify options to achieve more advanced debugging workflow. Currently, the debugger supports 4 options: + +- `do: ` - when the breakpoint is triggered, execute the given command/expression and continue the program: + - `break Foo#bar do: bt` - when `Foo#bar` is called, print the stack frames +- `pre: ` - when the breakpoint is triggered, execute the given command/expression before stopping: + - `break Foo#bar pre: info` - when `Foo#bar` is called, print its surrounding variables before stopping. +- `if: ` - the breakpoint only stops if the result of ` is true: + - `break Post#save if: params[:debug]` - stops at `Post#save` if `params[:debug]` is also true +- `path: ` - the breakpoint only stops if the event that triggers it (e.g. a method call) happens from the given path: + - `break Post#save if: app/services/a_service` - stops at `Post#save` if the method call happens at a method matches Ruby regexp `/app\/services\/a_service/`. + +Please also note that the first 3 options: `do:`, `pre:` and `if:` are also available for the debug statements we mentioned earlier. For example: + +```rb +[2, 11] in ~/projects/rails-guide-example/app/controllers/posts_controller.rb + 2| before_action :set_post, only: %i[ show edit update destroy ] + 3| + 4| # GET /posts or /posts.json + 5| def index + 6| @posts = Post.all +=> 7| debugger(do: "info") + 8| end + 9| + 10| # GET /posts/1 or /posts/1.json + 11| def show +=>#0 PostsController#index at ~/projects/rails-guide-example/app/controllers/posts_controller.rb:7 + #1 ActionController::BasicImplicitRender#send_action(method="index", args=[]) at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/actionpack-7.0.0.alpha2/lib/action_controller/metal/basic_implicit_render.rb:6 + # and 72 frames (use `bt' command for all frames) +(rdbg:binding.break) info +%self = # +@_action_has_layout = true +@_action_name = "index" +@_config = {} +@_lookup_context = # +@_response = #... +@_response_body = nil +@_routes = nil +@marked_for_same_origin_verification = true +@posts = # 80| raise(RecordInvalid.new(self)) + 81| end + 82| + 83| def perform_validations(options = {}) + 84| options[:validate] == false || valid?(options[:context]) +=>#0 ActiveRecord::Validations#raise_validation_error at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/activerecord-7.0.0.alpha2/lib/active_record/validations.rb:80 + #1 ActiveRecord::Validations#save!(options={}) at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/activerecord-7.0.0.alpha2/lib/active_record/validations.rb:53 + # and 88 frames (use `bt' command for all frames) +``` + +Once the catch breakpoint is triggered, it'll print the stack frames + +```rb +Stop by #0 BP - Catch "ActiveRecord::RecordInvalid" + +(rdbg:catch) bt 10 +=>#0 ActiveRecord::Validations#raise_validation_error at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/activerecord-7.0.0.alpha2/lib/active_record/validations.rb:80 + #1 ActiveRecord::Validations#save!(options={}) at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/activerecord-7.0.0.alpha2/lib/active_record/validations.rb:53 + #2 block in save! at ~/.rbenv/versions/3.0.1/lib/ruby/gems/3.0.0/gems/activerecord-7.0.0.alpha2/lib/active_record/transactions.rb:302 +``` + +This technique can save you from repeated manual input and make the debugging experience smoother. + You can find more commands and configuration options from its [documentation](https://github.com/ruby/debug). #### Autoloading Caveat