debug.gemを色々ためして、使いこなせるようになる

debug.gemが最近どんどん進化しているらしいので、きちんと使いこなして生産性を上げたいなと思っていました。
しかし、コマンド一覧を見てもいまいちどのタイミングでどれを使えばいいのかが分からなかったので、「oooをしたい」→「xxxのコマンド」のようにコマンドをまとめてみました。

:::note
debug v1.8.0
:::

サンプルとして適当なコードを実装して、いろいろ試してみました。

<details><summary>アプリケーションコード</summary>

```app/models/user.rb
class User < ApplicationRecord
  has_many :articles, dependent: :destroy

  def category
    case articles.published.size
    when 0..1
      light
    when 2..3
      normal
    else
      heavy
    end
  end

  def light
    :normal
  end

  def normal
    :normal
  end

  def heavy
    :heavy
  end
end
```
</details>

<details><summary>テストコード</summary>

```spec/models/user_spec.rb
require 'rails_helper'

RSpec.describe User, type: :model do
  let(:user) { User.create(name: 'user name') }

  describe '#category' do
    subject { user.category }

    context 'when the user has no articles' do
      it do
        is_expected.to eq :light
      end
    end
  end
end

```
</details>

## ブレイクポイントを仕込む

:::note
`binding.b` / `binding.break` / `debugger` のいずれかを記述します
:::

```diff_ruby
  describe '#category' do
    subject { user.category }

    context 'when the user has no articles' do
      it do
+       binding.b
        is_expected.to eq :light
      end
    end
  end
```

RSpecを実行するとこう表示されます。

```bash
[6, 15] in ~/git/sample-app/spec/models/user_spec.rb
     6|   describe '#category' do
     7|     subject { user.category }
     8|
     9|     context 'when the user has no articles' do
    10|       it do
=>  11|         binding.b
    12|         is_expected.to eq :light
    13|       end
    14|     end
    15|
=>#0	block in <top (required)> (4 levels) at ~/git/sample-app/spec/models/user_spec.rb:11
  #1	[C] BasicObject#instance_exec at ~/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/rspec-core-3.12.2/lib/rspec/core/example.rb:263
  # and 55 frames (use `bt' command for all frames)
```

## ローカル変数やインスタンス変数、定数などを調べる

:::note
i / info
:::

```bash
(rdbg) i    # info command
%self = #<RSpec::ExampleGroups::User::Category::WhenTheUserHasNoArticles "example at ./spec/models/user_spec.rb:10">
@__inspect_output = "\"example at ./spec/models/user_spec.rb:10\""
@__memoized = #<RSpec::Core::MemoizedHelpers::ThreadsafeMemoized:0x000000010a07b218 @memoized={}, @mutex=#<RSpec::Support::ReentrantMutex:0x000000010a07b1a0 @owner=nil, @count=0, @mutex=#<RSpec::Suppo...>
@connection_subscriber = #<ActiveSupport::Notifications::Fanout::Subscribers::Timed:0x000000010ad3d2a8 @pattern="!connection.active_record", @delegate=#<Proc:0x000000010a077960 /Users/kyntk/.rbenv/ver...>
@example = nil
@fixture_cache = {}
@fixture_connections = [#<ActiveRecord::ConnectionAdapters::SQLite3Adapter:0x000000010acb5830 @memory_database=false, @transaction_manager=#<ActiveRecord::ConnectionAdapters::TransactionManager:0x00000...
@legacy_saved_pool_configs = {}
@loaded_fixtures = {}
@saved_pool_configs = {"ActiveRecord::Base"=>{:default=>{}}}
```

`i i`（インスタンス変数）や`i l`（ローカル変数）などで必要な情報だけを絞って確認できます。

## 新たなブレイクポイントを仕込む

最初に設定したブレイクポイントだけでなく、別の場所にブレイクポイントを仕込むことができます。
これを使うことによって、ソースコードを変更してから再度実行をする手間を省けます。

:::note
b / break
:::

```bash
(rdbg) b user.category    # break command
#0  BP - Method  user.category at /Users/kyntk/git/sample-app/app/models/user.rb:6
(rdbg) b    # break command
#0  BP - Method  user.category at /Users/kyntk/git/sample-app/app/models/user.rb:6
```

行数やメソッドを指定してブレイクポイントを仕込むことができます。引数を渡さない場合はブレイクポイントの一覧を確認できます。


(helpから一部引用)

```md
### 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>`.
```

## プログラムを進める

:::note
- c / continue
- n / next
- s / step
:::

これらを用いて、デバッグをしたいところへ進みます。

```bash
(rdbg) s    # step command
[11, 20] in ~/git/sample-app/app/models/user.rb
    11|       heavy
    12|     end
    13|   end
    14|
    15|   def light
=>  16|     :normal
    17|   end
    18|
    19|   def normal
    20|     :normal
=>#0	User#light at ~/git/sample-app/app/models/user.rb:16
  #1	User#category at ~/git/sample-app/app/models/user.rb:7
  # and 68 frames (use `bt' command for all frames)
```

step inした後、そのフレーム内だけを進めたい場合は `fin / finish`を使います。

## バックトレースを遡る

:::note
- bt / backtrace
- f / frame
- up
- down
:::

まず `bt` を使ってバックトレースを確認します。
バックトレースは長いので、引数で表示する数を渡します。

```bash
(rdbg) bt 5    # backtrace command
=>#0	User#light at ~/git/sample-app/app/models/user.rb:16
  #1	User#category at ~/git/sample-app/app/models/user.rb:7
  #2	block in <top (required)> (3 levels) at ~/git/sample-app/spec/models/user_spec.rb:7
  #3	block in let (2 levels) at ~/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/rspec-core-3.12.2/lib/rspec/core/memoized_helpers.rb:343
  #4	block in fetch_or_store (3 levels) at ~/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/rspec-core-3.12.2/lib/rspec/core/memoized_helpers.rb:179
  # and 65 frames (use `bt' command for all frames)
```

この出力を確認して、フレームを指定して移動できます。

```bash
(rdbg) f 1    # frame command
=>   7|       light
=>#1	User#category at ~/git/sample-app/app/models/user.rb:7
(rdbg) i    # info command

略

(rdbg)
(rdbg)
(rdbg) f 2    # frame command
=>   7|     subject { user.category }
=>#2	block in <top (required)> (3 levels) at ~/git/sample-app/spec/models/user_spec.rb:7
(rdbg) i    # info command

略

(rdbg)
```

## 周辺のソースコードを確認する

:::note
- whereami
- l / list
:::

```ruby
(rdbg) whereami    # command
[2, 11] in ~/git/sample-app/spec/models/user_spec.rb
     2|
     3| RSpec.describe User, type: :model do
     4|   let(:user) { User.create(name: 'user name') }
     5|
     6|   describe '#category' do
=>   7|     subject { user.category }
     8|
     9|     context 'when the user has no articles' do
    10|       it do
    11|         binding.b
  #0	User#light at ~/git/sample-app/app/models/user.rb:16
  #1	User#category at ~/git/sample-app/app/models/user.rb:7
  # and 68 frames (use `bt' command for all frames)
```

`l`を複数回押すと、どんどんソースコードを下に読み進み、`l -`で上に登って確認できます。行数を指定することもできます。

```ruby
(rdbg) l    # list command
     2|
     3| RSpec.describe User, type: :model do
     4|   let(:user) { User.create(name: 'user name') }
     5|
     6|   describe '#category' do
=>   7|     subject { user.category }
     8|
     9|     context 'when the user has no articles' do
    10|       it do
    11|         binding.b
(rdbg)
(rdbg)
(rdbg)
(rdbg)
(rdbg)
(rdbg) l 1-20    # list command
     1| require 'rails_helper'
     2|
     3| RSpec.describe User, type: :model do
     4|   let(:user) { User.create(name: 'user name') }
     5|
     6|   describe '#category' do
=>   7|     subject { user.category }
     8|
     9|     context 'when the user has no articles' do
    10|       it do
    11|         binding.b
    12|         is_expected.to eq :light
    13|       end
    14|     end
    15|   end
    16| end
```

## 繰り返し同じデバッグを行う

これはブレイクポイントで行うというものではなく、ブレイクポイントの仕込み方についてです。

binding.bの引数に`do`, `pre`のキーワードをつけると、ブレイクポイントを仕込むのと合わせて一連のコマンドをまとめて実行することができます。

do, preの違いとしては`binding.b`を設置したポイントで止まる(`pre`)かそのまま処理を継続するか(`do`)です。
`;;`は複数のコマンドを区切ってつなげる時に使います。

```diff_ruby
  describe '#category' do
    subject { user.category }

    context 'when the user has no articles' do
      it do
-       binding.b
+       binding.b(do: 'i ;; b user.category')
        is_expected.to eq :light
      end
    end
  end
```

下記の出力の途中で、 `(rdbg:#debugger)`となっているところがありますが、ここでは入力を待たずに処理が進みます。

```bash
$ bundle exec rspec spec/models/user_spec.rb
[6, 15] in ~/git/sample-app/spec/models/user_spec.rb
     6|   describe '#category' do
     7|     subject { user.category }
     8|
     9|     context 'when the user has no articles' do
    10|       it do
=>  11|         binding.b(do: 'i ;; b user.category')
    12|         is_expected.to eq :light
    13|       end
    14|     end
    15|
=>#0	block in <top (required)> (4 levels) at ~/git/sample-app/spec/models/user_spec.rb:11
  #1	[C] BasicObject#instance_exec at ~/.rbenv/versions/3.2.2/lib/ruby/gems/3.2.0/gems/rspec-core-3.12.2/lib/rspec/core/example.rb:263
  # and 55 frames (use `bt' command for all frames)
(rdbg:#debugger) i
%self = #<RSpec::ExampleGroups::User::Category::WhenTheUserHasNoArticles "example at ./spec/models/user_spec.rb:10">
@__inspect_output = "\"example at ./spec/models/user_spec.rb:10\""

略

(rdbg:#debugger) b user.category
#0  BP - Method  user.category at /Users/kyntk/git/sample-app/app/models/user.rb:4
[1, 10] in ~/git/sample-app/app/models/user.rb
     1| class User < ApplicationRecord
     2|   has_many :articles, dependent: :destroy
     3|
     4|   def category
=>   5|     case articles.published.size
     6|     when 0..1
     7|       light
     8|     when 2..3
     9|       normal
    10|     else
=>#0	User#category at ~/git/sample-app/app/models/user.rb:5
  #1	block in <top (required)> (3 levels) at ~/git/sample-app/spec/models/user_spec.rb:7
  # and 67 frames (use `bt' command for all frames)

Stop by #0  BP - Method  user.category at /Users/kyntk/git/sample-app/app/models/user.rb:4
(rdbg)
```

これにより、デバッグの計画を事前にできたり、補完やスニペットとして使いまわせたり、他の人の環境でも同じようにデバッグを再現できたりというメリットがあるようです。[^1]

[^1]: https://github.com/st0012/slides/blob/main/2022-09-08-rubykaigi/Ruby%20debugger%20-%20The%20best%20investment%20for%20your%20productivity.pdf

## 特定の条件でブレイクポイントを設置する

`if`キーワードを使ったり、`catch`でエラー発生時にブレイクポイントを設置できます。

```diff_ruby
    context 'when the user has no articles' do
      it do
-       binding.b(do: 'i ;; b user.category')
+       binding.b(do: 'i ;; b user.category if: self.name == "user name"')
        is_expected.to eq :light
      end
    end
```

```diff_ruby
    context 'when the user has no articles' do
      it do
-       binding.b(do: 'i ;; b user.category')
+       binding.b(do: 'i ;; catch ActiveRecord::RecordNotFound')
+
+       User.find(1000) # エラーが発生する処理
        is_expected.to eq :light
      end
    end
```

`catch`も`break`と同様に、`pre`, `do`を使ってコマンドを組み合わせることができます。

```diff_ruby
    context 'when the user has no articles' do
      it do
-       binding.b(do: 'i ;; catch ActiveRecord::RecordNotFound')
+       binding.b(do: 'i ;; catch ActiveRecord::RecordNotFound pre: i')

        User.find(1000) # エラーが発生する処理
        is_expected.to eq :light
      end
    end
```

デバッグの際にいちいち調べてやるのは非効率なので、これらがスムーズにできるようになりたいです。

## 参考

https://github.com/ruby/debug

https://rubykaigi.org/2022/presentations/_st0012.html#day2