8
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

お題は不問!Qiita Engineer Festa 2023で記事投稿!

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

Posted at

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

debug v1.8.0

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

アプリケーションコード
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
テストコード
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

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

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

  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を実行するとこう表示されます。

[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)

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

i / info

(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(ローカル変数)などで必要な情報だけを絞って確認できます。

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

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

b / break

(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から一部引用)

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

プログラムを進める

  • c / continue
  • n / next
  • s / step

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

(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を使います。

バックトレースを遡る

  • bt / backtrace
  • f / frame
  • up
  • down

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

(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)

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

(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)

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

  • whereami
  • l / list
(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 -で上に登って確認できます。行数を指定することもできます。

(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)です。
;;は複数のコマンドを区切ってつなげる時に使います。

  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)となっているところがありますが、ここでは入力を待たずに処理が進みます。

$ 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

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

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

    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
    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

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

    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

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

参考

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

8
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?