12/08/2018, 14:47

Một số thủ thuật Debug code Ruby

Bài viết này bao gồm một số thủ thuật để giúp các bạn có thể tìm hiểu và debug code ruby một cách hiệu quả hơn. Tìm vị trí mà một method được định nghĩa Sử dụng hàm method của Object: object = Object . new puts object . method ( :blank? ) . source_location = > / home ...

Bài viết này bao gồm một số thủ thuật để giúp các bạn có thể tìm hiểu và debug code ruby một cách hiệu quả hơn.

Tìm vị trí mà một method được định nghĩa

Sử dụng hàm method của Object:

object = Object.new
puts object.method(:blank?).source_location
=> /home/hatt/.rvm/gems/ruby-2.3.1/gems/activesupport-5.0.0/lib/active_support/core_ext/object/blank.rb
=> 14

Hàm blank? này được định nghĩa ở dòng 14 của file active_support/core_ext/object/blank.rb.

Mở thư mục của một gem được sử dụng trong hệ thống

bundle open active_support

Câu lệnh trên sẽ mở thư mục chứa một phiên bản của active_support được định nghĩa trong Gemfile.lock. Từ đây, bạn có thể đặt các câu lệnh debug (pry, bye_bug...) vào code và tìm hiểu xem gem hoạt động như thế nào. Câu lệnh này sẽ sử dụng giá trị từ biến môi trường EDITOR. Giá trị mặc định có thể là vi hoặc bạn có thể thiết lập nó trong .bashrc.

Reset Gem về lại thời điểm ban đầu

Nếu bạn mở một gem và chỉnh sửa trong đó nhưng lại quên không "check out" lại, những thay đổi của bạn sẽ ảnh hưởng đến những lần chạy sau của gem. Để reset về lại như ban đầu, bạn có thể chạy lệnh gem pristine. Để reset gem active_support, ta làm như sau:

$ gem pristine activesupport

Để reset toàn bộ gem, bạn có thể chạy lệnh sau:

$ gem pristine --all

Câu lệnh trên có thể sẽ tốn nhiều thời gian, đặc biệt nếu như bạn có gem với c-extensions

Tìm hiểu xem một hàm được gọi như thế nào

Để tạo ra một backtrace mà không muốn có exception, các bạn có thể dùng hàm caller như sau:

class Project
  def foo
    puts caller
  end
end

Khi hàm foo được gọi thì nó sẽ sinh ra một backtrace tương tự như sau:

=====================
(pry):8:in `__pry__'
/home/hatt/.rvm/gems/ruby-2.3.1/gems/pry-0.10.3/lib/pry/pry_instance.rb:355:in `eval'
/home/hatt/.rvm/gems/ruby-2.3.1/gems/pry-0.10.3/lib/pry/pry_instance.rb:355:in `evaluate_ruby'
/home/hatt/.rvm/gems/ruby-2.3.1/gems/pry-0.10.3/lib/pry/pry_instance.rb:323:in `handle_line'
/home/hatt/.rvm/gems/ruby-2.3.1/gems/pry-0.10.3/lib/pry/pry_instance.rb:243:in `block (2 levels) in eval'
/home/hatt/.rvm/gems/ruby-2.3.1/gems/pry-0.10.3/lib/pry/pry_instance.rb:242:in `catch'
/home/hatt/.rvm/gems/ruby-2.3.1/gems/pry-0.10.3/lib/pry/pry_instance.rb:242:in `block in eval'
/home/hatt/.rvm/gems/ruby-2.3.1/gems/pry-0.10.3/lib/pry/pry_instance.rb:241:in `catch'
/home/hatt/.rvm/gems/ruby-2.3.1/gems/pry-0.10.3/lib/pry/pry_instance.rb:241:in `eval'
/home/hatt/.rvm/gems/ruby-2.3.1/gems/pry-0.10.3/lib/pry/repl.rb:77:in `block in repl'
/home/hatt/.rvm/gems/ruby-2.3.1/gems/pry-0.10.3/lib/pry/repl.rb:67:in `loop'
/home/hatt/.rvm/gems/ruby-2.3.1/gems/pry-0.10.3/lib/pry/repl.rb:67:in `repl'
/home/hatt/.rvm/gems/ruby-2.3.1/gems/pry-0.10.3/lib/pry/repl.rb:38:in `block in start'
/home/hatt/.rvm/gems/ruby-2.3.1/gems/pry-0.10.3/lib/pry/input_lock.rb:61:in `__with_ownership'
/home/hatt/.rvm/gems/ruby-2.3.1/gems/pry-0.10.3/lib/pry/input_lock.rb:79:in `with_ownership'
/home/hatt/.rvm/gems/ruby-2.3.1/gems/pry-0.10.3/lib/pry/repl.rb:38:in `start'
/home/hatt/.rvm/gems/ruby-2.3.1/gems/pry-0.10.3/lib/pry/repl.rb:15:in `start'
/home/hatt/.rvm/gems/ruby-2.3.1/gems/pry-0.10.3/lib/pry/pry_class.rb:169:in `start'
/home/hatt/.rvm/gems/ruby-2.3.1/gems/railties-5.0.0/lib/rails/commands/console.rb:65:in `start'
/home/hatt/.rvm/gems/ruby-2.3.1/gems/railties-5.0.0/lib/rails/commands/console_helper.rb:9:in `start'
/home/hatt/.rvm/gems/ruby-2.3.1/gems/railties-5.0.0/lib/rails/commands/commands_tasks.rb:78:in `console'
/home/hatt/.rvm/gems/ruby-2.3.1/gems/railties-5.0.0/lib/rails/commands/commands_tasks.rb:49:in `run_command!'
/home/hatt/.rvm/gems/ruby-2.3.1/gems/railties-5.0.0/lib/rails/commands.rb:18:in `<top (required)>'
bin/rails:4:in `require'
bin/rails:4:in `<main>'

Dòng đầu tiên là chỉ hàm được gọi trước khi puts caller được gọi. Các bạn có thể thấy là hàm foo của tôi được gọi tử pry console dựa vào dòng đầu tiên của backtrace phía trên:

(pry):8:in `__pry__`

Nều bạn để ý toàn bộ backtrace thì sẽ thấy cách mà pry hoạt động và xử lý input đưa vào console như thế nào (có thể ở một bài viết khác sẽ đề cập tới vấn đề này).

Tìm xem super được gọi từ đâu

Trong ruby bạn có thể include nhiều module vào một object, mà đội khi các module cũng có thể có các hàm bị trùng tên mà bạn lại sử dụng super trên những hàm này. Vì vậy, việc tìm source location của super khá là hữu ích. Các bạn có thể sử dụng các sau:

def foo
  puts method(:foo).super_method.source_location
  super
end

Liệt kê toàn bộ các hàm của object

object.methods

Tìm vị trí của một hàm của instance mà không phải tạo một instance mới

User.instance_method(:foo).source_location

Liệt kê các tham số của hàm

def parse(input, skip_code_comments: false, ignore_whitespace: true)
  #...
end
method(:parse).parameters

# => [[:req, :input], [:key, :skip_code_comments], [:key, :ignore_whitespace]]

Tìm vị trí mà một tham số bị thay đổi giá trị

Thông thường khi chúng ta khởi tạo một biến như sau:

config.thing = {"foo" => "bar"}

Tuy nhiên, ở những đoạn code sau đó thì giá trị của nó lại bị thay đổi như sau:

puts config.thing
# => {"foo" => "THIS VALUE IS DIFFERENT"}

Các bạn có thể xem đoạn code nào thay đổi giá trih này bằng cách sử dụng freeze lên chính giá trị đó. Đoạn code nào thay đổi giá trị chắc chắn sẽ bắn ra exceptions và có backtrace để ta thấy được vị trí mà giá trị bị thay đổi.

Kỹ thuật này sẽ không hoạt động trong trường hợp biến được gán giá trị mới tuy nhiên nó cũng đáng để thử và giúp ta loại trừ được một trường hợp khi debug.

Tìm vị trí mà constant được tạo

Các bạn có thể sử dụng object space để xem một constant hoặc object được khởi tạo ở đâu:

require 'objspace'
ObjectSpace.trace_object_allocations_start

Kernel.send(:define_method, :sup) do |obj|
  puts "#{ ObjectSpace.allocation_sourcefile(obj) }:#{ ObjectSpace.allocation_sourceline(obj) }"
end

world = "hello"

sup world

# => (pry):14

Do đoạn code để lấy tên file và số dòng khá dài nên hàm sup được định nghĩa để thu gọn đoạn code đó lại. Các bạn có thể thấy là object world được định nghĩa trong pry console dòng 14.

Kết bài

Trên đây là những thủ thuật giúp các bạn có thể debug hoặc đọc và tìm hiểu xem code ruby được chạy như thế nào trong dự án của mình cũng như là trong những gem chúng ta hay sử dụng. Nếu các bạn muốn tìm hiểu sâu về source code của gem mình đang sử dụng thì những thủ thuật trên sẽ rất hữu ích đối với bạn.

References: Ruby Debugging Magic Cheat Sheet

0