debug.gemが最近どんどん進化しているらしいので、きちんと使いこなして生産性を上げたいなと思っていました。
しかし、コマンド一覧を見てもいまいちどのタイミングでどれを使えばいいのかが分からなかったので、「oooをしたい」→「xxxのコマンド」のようにコマンドをまとめてみました。
debug v1.8.0
サンプルとして適当なコードを実装して、いろいろ試してみました。
アプリケーションコード
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
テストコード
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
catch
もbreak
と同様に、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
デバッグの際にいちいち調べてやるのは非効率なので、これらがスムーズにできるようになりたいです。
参考