如何用ruby/debug调试Rails 7

550 阅读6分钟

1.前提条件

检查你是否已经安装了ruby 3。检查你是否已经安装了bundler,以及版本7以上的npm。

$> ruby -v  
ruby 3.0.0p0 // you need at least version 3 here  
$> bundle -v  
Bundler version 2.2.11

任何更高的版本都可以使用。

2.安装最小的网络应用程序

$> mkdir myapp && cd myapp  
$/myapp> echo "source 'https://rubygems.org'" > Gemfile  
$/myapp> echo "gem 'rails', '7.0.0'" >> Gemfile  
$/myapp> bundle install  
$/myapp> bundle exec rails new . --force
$/myapp> bundle update  
$/myapp> bin/rails db:create  
$/myapp> bin/rails db:migrate

3.探索Gemfile

source "https://rubygems.org"
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby "3.0.0"

gem "rails", "~> 7.0.0"
gem "sprockets-rails"
gem "pg", "~> 1.1"
gem "puma", "~> 5.0"
gem "tzinfo-data", platforms: %i[ mingw mswin x64_mingw jruby ]

group :development, :test do
  gem "debug", platforms: %i[ mri mingw x64_mingw ]
end

看到最后3行了吗?** 在任何Rails应用程序中,调试宝石都是默认可用的**,即使是最小化的应用程序,这意味着Rails维护者认为这个调试宝石是你不能缺少的东西。

4.Scaffold视图、Rails和模型

为了快速测试(以及创建管理界面),Rails提供了脚手架:在一个资源上完全创建CRUD操作。

$/myapp> bin/rails generate scaffold Computer name:string price:integer
      invoke  active_record
      create    db/migrate/20211222182724_create_computers.rb
      create    app/models/computer.rb
      invoke    test_unit
      create      test/models/computer_test.rb
      create      test/fixtures/computers.yml
      invoke  resource_route
       route    resources :computers
      invoke  scaffold_controller
      create    app/controllers/computers_controller.rb
      invoke    erb
      create      app/views/computers
      create      app/views/computers/index.html.erb
      create      app/views/computers/edit.html.erb
      create      app/views/computers/show.html.erb
      create      app/views/computers/new.html.erb
      create      app/views/computers/_form.html.erb
      create      app/views/computers/_computer.html.erb
      invoke    resource_route
      invoke    test_unit
      create      test/controllers/computers_controller_test.rb
      invoke    helper
      create      app/helpers/computers_helper.rb
      invoke      test_unit

我们将在后面写到脚手架的内容。现在,我们只是注意到Rails为我们创建了所有的骨架。非常方便。

然后打开config/routes.rb

# inside config/routes.rb
Rails.application.routes.draw do
  resources :computers
  root to: "computers#index"
end

然后运行迁移:

$> bin/rails db:migrate

并启动你的本地Web服务器

$> bin/rails server 

最后打开你的浏览器http://localhost:3000

localhost

Localhost

很好!一切顺利。让我们看看如何调试这个应用程序,以免出现任何突然的错误。

5.调试我们的应用程序

首先打开app/controllers/computers_controller.rb

class ComputersController < ApplicationController
  before_action :set_computer, only: %i[ show edit update destroy ]

  # GET /computers
  def index
    @computers = Computer.all
  end
  
  # A lot more code...
end

从注释中,你可以猜到,每次在浏览器中输入/computers URL时,都会调用index函数。很好!这让我们可以轻松地尝试ruby/debuggem。

修改app/controllers/computers_controller.rb如下

class ComputersController < ApplicationController
  before_action :set_computer, only: %i[ show edit update destroy ]

  # GET /computers
  def index
    my_age = 42
    binding.break   
    @computers = Computer.all
    binding.break
  end
  
  # A lot more code...
end

所以我们添加了2行binding.break

从名字上看,我们可以猜测Rails服务器应该在每次到达binding.break 指令时停止。

到达一个断点

停止你的本地Web服务器。用.NET重新启动它。

$> bin/rails server 

并在http://localhost:3000,刷新你的浏览器

你应该看到表示你的页面现在无法加载的旋钮。

spinner

旋转器

可能断点确实已经到达了,就像计划的那样 !

打开你的终端。

beautiful colors in terminal

终端中的美丽色彩

程序已经在你要求的地方停止了:在binding.break 指令上。

第一课 :

ruby/debug允许我们在终端中调试Ruby-on-Rails程序,它具有你通常在IDE中找到的所有功能:下一步、评估、继续,等等。

第二课 :

ruby/debug带有漂亮的颜色,这对我们帮助很大。

评估一个变量

在你的终端中,光标已经定位在 ruby 调试器中。

键入

(rdbg) my_age # ruby
42
(rdbg) @computers
nil
(rdbg)

my_age 已经存在,并且调试器向我们显示了它的值。很好 !当我们到达第一个断点时,@computers还没有被设置,所以它的值是 (现在)。nil

设置一个变量

(rdbg) eval my_age=43    # command
(rdbg) my_age    # ruby
43
(rdbg)

如果你想在你的控制器或服务对象中达到一个特定的状态,这非常有用。还要注意每行末尾的注释。

第三课 :

ruby/debug 使得实时读取和写入任何变量成为可能,使你的 Rails 应用程序能够达到任何想要的状态。

第四课 :

ruby/debug 有一个很好的开发者用户体验,它试图对现有的命令进行注释(如果没有注释出现,你就试图输入一个不存在的命令...)。

跳到下一个断点

(rdbg) c    # continue command
[4, 13] in ~/workspace/myapp/app/controllers/computers_controller.rb
     4|   # GET /computers
     5|   def index
     6|     my_age = 42
     7|     binding.break   
     8|     @computers = Computer.all
=>   9|     binding.break   
    10|   end

现在,毫不奇怪,如果你评估@computers ,它就存在。

(rdbg) @computers # ruby
[]
(rdbg) 3 + 4 # ruby
7

注意,你可以输入任何Ruby表达式,而不仅仅是现有的变量。

逃离调试器

输入 "c "和 "enter",直到你从所有断点中逃脱。

(rdbg) c    # continue command
  Rendering layout layouts/application.html.erb
  Rendering computers/index.html.erb within layouts/application
  Rendered computers/index.html.erb within layouts/application (Duration: 0.7ms | Allocations: 327)
  Rendered layout layouts/application.html.erb (Duration: 4.4ms | Allocations: 1317)
Completed 200 OK in 1581101ms (Views: 9.0ms | ActiveRecord: 9.0ms | Allocations: 139408)

然后回到浏览器,在http://locahost:3000。旋转器已经消失了。如果你重新加载页面,旋转器又出现了,你可以在终端享受一个新的调试会话。

所有可用的选项

在调试器内,输入 "h"

(rdbg) h    # help command
### Control flow

* `s[tep]`
  * Step in. Resume the program until next breakable point.
* `s[tep] <n>`
  * Step in, resume the program at `<n>`th breakable point.
* `n[ext]`
  * Step over. Resume the program until next line.
* `n[ext] <n>`
  * Step over, same as `step <n>`.
* `fin[ish]`
  * Finish this frame. Resume the program until the current frame is finished.
* `fin[ish] <n>`
  * Finish `<n>`th frames.
* `c[ontinue]`
  * Resume the program.
* `q[uit]` or `Ctrl-D`
  * Finish debugger (with the debuggee process on non-remote debugging).
* `q[uit]!`
  * Same as q[uit] but without the confirmation prompt.
* `kill`
  * Stop the debuggee process with `Kernel#exit!`.
* `kill!`
  * Same as kill but without the confirmation prompt.
* `sigint`
  * Execute SIGINT handler registered by the debuggee.
  * Note that this command should be used just after stop by `SIGINT`.

### Breakpoint

* `b[reak]`
  * Show all breakpoints.
* `b[reak] <line>`
  * Set breakpoint on `<line>` at the current frame's file.
* `b[reak] <file>:<line>` or `<file> <line>`
  * Set breakpoint on `<file>:<line>`.
* `b[reak] <class>#<name>`
   * Set breakpoint on the method `<class>#<name>`.
* `b[reak] <expr>.<name>`
   * Set breakpoint on the method `<expr>.<name>`.
* `b[reak] ... if: <expr>`
  * break if `<expr>` is true at specified location.
* `b[reak] ... pre: <command>`
  * break and run `<command>` before stopping.
* `b[reak] ... do: <command>`
  * break and run `<command>`, and continue.
* `b[reak] ... path: <path_regexp>`
  * break if the triggering event's path matches <path_regexp>.
* `b[reak] if: <expr>`
  * break if: `<expr>` is true at any lines.
  * 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.
* `catch ... path: <path_regexp>`
  * stops if the exception is raised from a path that matches <path_regexp>.
* `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.
* `watch ... path: <path_regexp>`
  * stops if the triggering event's path matches <path_regexp>.
* `del[ete]`
  * delete all breakpoints.
* `del[ete] <bpnum>`
  * delete specified breakpoint.

### Information

* `bt` or `backtrace`
  * Show backtrace (frame) information.
* `bt <num>` or `backtrace <num>`
  * Only shows first `<num>` frames.
* `bt /regexp/` or `backtrace /regexp/`
  * Only shows frames with method name or location info that matches `/regexp/`.
* `bt <num> /regexp/` or `backtrace <num> /regexp/`
  * Only shows first `<num>` frames with method name or location info that matches `/regexp/`.
* `l[ist]`
  * Show current frame's source code.
  * Next `list` command shows the successor lines.
* `l[ist] -`
  * Show predecessor lines as opposed to the `list` command.
* `l[ist] <start>` or `l[ist] <start>-<end>`
  * Show current frame's source code from the line <start> to <end> if given.
* `edit`
  * Open the current file on the editor (use `EDITOR` environment variable).
  * Note that edited file will not be reloaded.
* `edit <file>`
  * Open <file> on the editor.
* `i[nfo]`
   * Show information about current frame (local/instance variables and defined constants).
* `i[nfo] l[ocal[s]]`
  * Show information about the current frame (local variables)
  * It includes `self` as `%self` and a return value as `%return`.
* `i[nfo] i[var[s]]` or `i[nfo] instance`
  * Show information about instance variables about `self`.
* `i[nfo] c[onst[s]]` or `i[nfo] constant[s]`
  * Show information about accessible constants except toplevel constants.
* `i[nfo] g[lobal[s]]`
  * Show information about global variables
* `i[nfo] ... </pattern/>`
  * Filter the output with `</pattern/>`.
* `i[nfo] th[read[s]]`
  * Show all threads (same as `th[read]`).
* `o[utline]` or `ls`
  * Show you available methods, constants, local variables, and instance variables in the current scope.
* `o[utline] <expr>` or `ls <expr>`
  * Show you available methods and instance variables of the given object.
  * If the object is a class/module, it also lists its constants.
* `display`
  * Show display setting.
* `display <expr>`
  * Show the result of `<expr>` at every suspended timing.
* `undisplay`
  * Remove all display settings.
* `undisplay <displaynum>`
  * Remove a specified display setting.

### Frame control

* `f[rame]`
  * Show the current frame.
* `f[rame] <framenum>`
  * Specify a current frame. Evaluation are run on specified frame.
* `up`
  * Specify the upper frame.
* `down`
  * Specify the lower frame.

### Evaluate

* `p <expr>`
  * Evaluate like `p <expr>` on the current frame.
* `pp <expr>`
  * Evaluate like `pp <expr>` on the current frame.
* `eval <expr>`
  * Evaluate `<expr>` on the current frame.
* `irb`
  * Invoke `irb` on the current frame.

### Trace

* `trace`
  * Show available tracers list.
* `trace line`
  * Add a line tracer. It indicates line events.
* `trace call`
  * Add a call tracer. It indicate call/return events.
* `trace exception`
  * Add an exception tracer. It indicates raising exceptions.
* `trace object <expr>`
  * Add an object tracer. It indicates that an object by `<expr>` is passed as a parameter or a receiver on method call.
* `trace ... </pattern/>`
  * Indicates only matched events to `</pattern/>` (RegExp).
* `trace ... into: <file>`
  * Save trace information into: `<file>`.
* `trace off <num>`
  * Disable tracer specified by `<num>` (use `trace` command to check the numbers).
* `trace off [line|call|pass]`
  * Disable all tracers. If `<type>` is provided, disable specified type tracers.
* `record`
  * Show recording status.
* `record [on|off]`
  * Start/Stop recording.
* `step back`
  * Start replay. Step back with the last execution log.
  * `s[tep]` does stepping forward with the last log.
* `step reset`
  * Stop replay .

### Thread control

* `th[read]`
  * Show all threads.
* `th[read] <thnum>`
  * Switch thread specified by `<thnum>`.

### Configuration

* `config`
  * Show all configuration with description.
* `config <name>`
  * Show current configuration of <name>.
* `config set <name> <val>` or `config <name> = <val>`
  * Set <name> to <val>.
* `config append <name> <val>` or `config <name> << <val>`
  * Append `<val>` to `<name>` if it is an array.
* `config unset <name>`
  * Set <name> to default.
* `source <file>`
  * Evaluate lines in `<file>` as debug commands.
* `open`
  * open debuggee port on UNIX domain socket and wait for attaching.
  * Note that `open` command is EXPERIMENTAL.
* `open [<host>:]<port>`
  * open debuggee port on TCP/IP with given `[<host>:]<port>` and wait for attaching.
* `open vscode`
  * open debuggee port for VSCode and launch VSCode if available.
* `open chrome`
  * open debuggee port for Chrome and wait for attaching.

### Help

* `h[elp]`
  * Show help for all commands.
* `h[elp] <command>`
  * Show help for the given command.
(rdbg) 

ruby/debug的替代品

你可以试试byebug gem,直接用一些 "print "语句进行调试。这也是非常有效的 !