Ryan Bigg - Debugging Ruby.pdf

21

Transcript of Ryan Bigg - Debugging Ruby.pdf

  • Debugging RubyDebugging Ruby code & Rails applications by example

    Ryan BiggThis book is for sale at http://leanpub.com/debuggingruby

    This version was published on 2013-11-10

    This is a Leanpub book. Leanpub empowers authors and publishers with the Lean Publishingprocess. Lean Publishing is the act of publishing an in-progress ebook using lightweight tools andmany iterations to get reader feedback, pivot until you have the right book and build traction onceyou do.

    2013 Ryan Bigg

  • Tweet This Book!Please help Ryan Bigg by spreading the word about this book on Twitter!The suggested hashtag for this book is #debuggingruby.Find out what other people are saying about the book by clicking on this link to search for thishashtag on Twitter:https://twitter.com/search?q=#debuggingruby

  • Contents

    Debugging . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1Basic Example #1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1Basic Example #2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3

    Debugging Rails . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7Workflow . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7Rails Example #1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7

  • DebuggingThe ability to debug code is arguably more important than the ability to write code. Written code issometimes not completely perfect the first time its written, especially when humans are involved inthe writing of that code. Humans make mistakes, and those mistakes can cost time and money. Beingable to fix those mistakes in an efficient manner is a core competency to being a good programmer.These mistakes are sometimes extremely simple; a typo here, a missing bracket there, defining codeoutside of the correct scope. Other times they involve interconnected bits of code and tracking downthe misbehaving cog (or cogs) in that wonderful machine can take some time.In this book, were going to go through some common coding mistakes that people make and wewill look at how to solve them. The deeper and deeper you get into this book, the more complicatedthe examples will get. All the examples in this book will be written in Ruby.

    Basic Example #1Lets take a look at our first very simple program with an error:

    class Carattr_accesssor :onend

    This is some fairly simple Ruby code. All were doing is defining a class called Car and then callingthe attr_accesssormethod inside that class which should define a setter method called on= wherewe can store a value, and a getter method called on where we can retrieve that value. We dontnecessarily care about the setting and getting just yet. All we care about is that our code works.Put this code into a new file called car.rb and try running it with ruby car.rb. Ideally, nothingshould be output to the terminal, because we have not told Ruby to output anything. Instead ofnothing, you should see this:

    car.rb:2:in `':undefined method `attr_accesssor' for Car:Class (NoMethodError)from car.rb:1:in `'

    The output here is modified slightly to fit into the book. The first two lines in the above outputshould be one line. What has been output here is whats called a stacktrace or backtrace. This iswhat is output by a Ruby program when it fails. It shows us the execution flow of the program inreverse order. Lets start from the final line of the stacktrace:

  • Debugging 2

    from car.rb:1:in `'

    This line tells us that the execution flow of the program started on line 1 of the car.rb file. The here indicates the main namespace that all code within Ruby operates in. This line of thestacktrace is not too important. All its indicating is that a new container has been entered into. Inthis case, that container is a new class definition for the Car class.The first line of the stacktrace begins with this:

    car.rb:2:in `'

    This line tells us that on line 2 of car.rb, were operating within the scope of a class called Car(). Because this is the top line of the stacktrace, this is probably where our error hasoccurred. In most cases, the top line of the stacktrace will point to the exact line where an erroroccurs.The remainder of this line is this:

    undefined method `attr_accesssor' for Car:Class (NoMethodError)

    This part of it tells us the error thats happened. In this case, were trying to call a method that Rubyhas no knowledge of, the attr_accesssormehtod. Were trying to call that method on the Car class,but that method does not exist there. When we try to call a method that Ruby does not know about,it raises a NoMethodError exception, which is shown in brackets at the end of the line.Our program is 3 lines long, but the third line is just an end, and those are never included instacktraces because theyre just there to demarcate the end of a scope within the code, rather thanperform a function.From this stacktrace, we can gather that the error is happening on line 2. All were doing on line 2is this:

    attr_accesssor :on

    Do you see the mistake weve made yet? It might be glaringly obvious or it might not be. Look at itfor a moment longer. Do you see it now?The mistake weve made is dead simple: weve typed three ses rather than two. Weve done attr_-accesssor, rather than attr_accessor. The code should be this:

    attr_accessor :on

  • Debugging 3

    Change the code on line 2 to that now and re-run the program. It should run without showinganything. This is the standard for Ruby programs when they execute successfully. Unless we havespecifically told it to output anything i.e. with a call to the puts method nothing will happen.

    Verify program successYou can verify that a program has run successfully by running this command (assumingyoure using Bash or a similar shell):

    1 echo $?

    If that outputs 0, then everything has worked correctly.TODO: Bash error codes mention goes here.

    Now that weve fixed that error, lets cover another.

    Basic Example #2Look at this code:

    class Carattr_accessor :ondef start

    on = trueendendcar = Car.newcar.startputs car.on

    Can you tell what its doing just by looking at it? It defines a new class called Car, and a virtualattribute inside that class by way of attr_accessor called on. This time, attr_accessor is spelledcorrectly. Remember that the attr_accessor call defines a getter method called on and a settermethod called on=.After the attr_accessor, the code defines a method called startwhich sets on to true. It then endsthe method and class definition.On the final two lines, the code creates a new instance of this class with Car.new and assigns it toa local variable, calls start which should do what its told, and finally outputs the value of car.onusing puts. Just by looking at this code, you may expect that when it is run that it will output true.Youd be wrong.Put this code into car.rb and try running it. This is what will happen:

  • Debugging 4

    $ ruby car.rb$

    Rather than nothing being output this time, a blank line has been output. This is because the programhas done what weve told it to do. It does not output true, even though the start method is settingon to true. This time we dont have a stacktrace to tell us what line the error is probably on. Wehave to step through the programs execution ourselves.Since there are no exceptions raised within the code, we can assume that the code itself is valid.Lets step through the final three lines of the code:

    car = Car.newcar.startputs car.on

    The first line here creates a new instance of the Car class. Thats Rubys code, so thats probably notthe source of our frustration. The second line calls the startmethod which sets on to true. The finalline just outputs the value of on, which isnt too special a task. The problem probably lies within oursecond line. Lets look at how that method is defined:

    def starton = trueend

    This code inside the method is not dissimilar to the first line of the previous example:

    car = Car.new

    When we call car = Car.newwere setting a local variable to the value of whatever Car.new returns.In the start method, were setting an on local variable to true. This is not our intention! Ourintention is to set the virtual attribute for the instance to true. Beware this very fine difference!Theres more than one way to set this virtual attribute. We can go through the setter method call,like this:

    def startself.on = trueend

    The caveat of this is that the on= method may define other behaviour. This is a rare occurrence inRuby code, but it can happen. In this case, its obvious that the on= method isnt redefined to addextra behaviour, so its OK to do this. Another way of doing it would be to set the instance variableof the same name, like this:

  • Debugging 5

    def start@on = trueend

    Doing it this way has the benefit of the code being shorter and there being no unintended side-effectswith a method. Using either the setter method or setting the instance variable are both legitimateways of solving the issue with our code. Go ahead and try both ways now. They should both causethe program to output this when its run:

    $ ruby car.rbtrue

    Great, so weve solved this problem. The issue here was that we were trying to set a local variableinside the method, rather than referencing the virtual attribute setter method or instance variable.With that out of the way, lets look at the possibility of a setter method behaving badly as we talkedabout before. Lets change our code to this:

    class Carattr_accessor :ondef start

    self.on = trueenddef on=(value)

    @on = !valueendendcar = Car.newcar.startputs car.on

    The startmethod is now using the setter method, on=, to set the value of the virtual attribute calledon. The setter method is overriden from the default here directly underneath that. It takes the valueand negates it, and then stores it on that @on instance variable. This means that when we call on,well get the opposite of what it should be: setting it to true will make it false and vice versa.Try running this code yourself now and youll see:

    $ ruby car.rbfalse

    The on= method is maliciously negating our value, giving us an unexpected outcome. If we set theinstance variable within the startmethod directly, this would bypass the on=method and we wouldsee the right value. The start method would look like this:

  • Debugging 6

    def start@on = trueend

    And the output of the program would look like this:

    $ ruby car.rbtrue

    Its extremely rare that a setter method will perform strange functions like this, but it still isworthwhile knowing that it can happen, just in case we come across a situation like this in ourdebugging experiences.

  • Debugging RailsRails applications are generally more complex than one or two files. More often than not, youllhave many different pieces of the application all working together: the routes, the controllers, themodels, the views and the helpers. In this chapter, well cover some examples of errors that peoplemake within Rails applications and how to fix them.Well be covering:

    The debug helper How to read log output How constant lookup in Ruby works

    WorkflowEach section below works through one particular problem in a Rails application. These Railsapplications are kept on GitHub at https://github.co m/radar/debugging_book_examples and arenumbered the same way as their examples; i.e. Example #1s code is within the directory called 1underneath the rails directory.To work through the examples of these Rails applications, youll need to clone that repository toyour computer:

    git clone git://github.com/radar/debugging_book_examples

    Each Rails application comes fitted with RSpec and Capybara tests which will pass once the codehas been fixed. We can run these tests to execute some code which wil check if the applicationis working. Running automated tests is just an easier way compared with manually viewing theapplication ourselves and saves time in the long run as our applications get more and more complex.Lets begin with the first example now.

    Rails Example #1In this first application example, we have a very basic Rails application. The thing that we want thisapplication to do right now is for the users to be able to go to the New Post page without anyissues, but unfortunately theres a problem thats preventing them from doing that which well seeshortly.Before we do anything with this application, lets install all the dependencies:

    https://github.com/radar/debugging_book_examples

  • Debugging Rails 8

    bundle install

    Now lets look at the automated test we have inside spec/features/posts_spec.rb inside theapplication, written using RSpec and Capybaras DSL:

    require 'spec_helper'feature "Posts" do

    it "can begin to create a new post" dovisit root_pathclick_link 'New Post'find_field("Title")find_field("Body")endend

    This test will pass if it can navigate to the root page, click New Post and see two fields, one forTitle and one for Body. If we run the test now, well see this error:

    1) Posts can begin to create a new postFailure/Error: click_link 'New Post'ActionView::Template::Error:

    First argument in form cannot contain nil or be empty# ./app/views/posts/new.html.erb:1:in `...'# ./spec/features/posts_spec.rb:6:in `block (2 levels) in '

    This is a typical failure output from RSpec, shown when code raises an exception. The first lineof the message shows us which test is failing. We should have a pretty good idea which test thatis out of the one test that we have so far. The particular exception that were seeing with this testis ActionView::Template::Error, and the message for that exception is First argument in formcannot contain nil or be empty. Well get to that in a moment.The two remaining lines in this output show some of the stacktrace for the error. It shows us thatposts_spec.rb line 6 does something that triggers some code in app/views/posts/new.html.erbto run, and thats where the error is (probably) occurring. According to the stacktrace, thats line 1of app/views/posts/new.html.erb.All this application has inside it is a PostsController which does nothing other than inherit fromApplicationController. This controller is routed to by the two routes within config/routes.rb:

  • Debugging Rails 9

    Bug::Application.routes.draw doroot "posts#index"resources :postsend

    We have a view at app/views/posts/index.html.erb which just contains a link to the new action:

    When we go to that new_post_path, it will render app/views/posts/new.html.erb which containsthis content:

    So thats all we have: some routes, a controller that does nothing special, a template at app/views/posts/index.html.erband another at app/views/posts/new.html.erb. Just four interconnected pieces in this application,nothing too special.Lets jump back to our test now and see what it said when we ran it with bundle exec rspecspec/features/posts_spec.rb.

    1) Posts can begin to create a new postFailure/Error: click_link 'New Post'ActionView::Template::Error:

    First argument in form cannot contain nil or be empty# ./app/views/posts/new.html.erb:1:in `...'# ./spec/features/posts_spec.rb:6:in `block (2 levels) in '

    The first line of the error is pointing directly to the first line in app/views/posts/new.html.erb:

  • Debugging Rails 10

    All were doing on this line is calling the form_formethod and passing it the @post instance variableand then starting a block. Later on in that file, were calling some methods on the block argument,but that doesnt really matter. The error is fair-and-square happening on this line. The most usefulpart of the error message is the message for the exception:

    First argument in form cannot contain nil or be empty

    The argument that its talking about here is the @post variable that were passing. Its claiming thatthe variable is nil or empty, and it probably is.

    The debug helperWe can see for ourselves if this variable is actually nil by removing all the code in app/views/posts/new.html.erband replacing it with this single line:

    The debug method here is provided by Rails and will output a YAMLized version of whatever it ispassed. Were passing to it the output of the @post.inspect call. The inspect method is a methodprovided by Ruby which outputs a human-friendly version of the object. If youve ever written Rubyin irb, youve seen the inspected versions of objects perhaps without even realising it.Start a new irb session now and try entering these things:

    1 1.inspect puts 1.inspect "1" "1".inspect puts "1".inspect [1,2,3] [1,2,3].inspect puts [1,2,3].inspect nil nil.inspect puts nil.inspect

  • Debugging Rails 11

    Well see that the non-inspect versions are almost identical to the inspect versions. The inspectversions just have more quotes around them. This is because the inspect call always returns aString object, as those are easiest for humans to read. Whatever the final thing is on the line forsome code entered into IRB is what will be returned in the IRB prompt. In the case of the putscalls, IRB returns nil because puts returns nil. IRB automatically uses inspect to return a human-friendly representation of whatever is entered into the prompt.The debugmethod in our view will not automatically call inspect on whatever it is passed. Instead,it calls another method: to_s. In some cases this method gives back similar output to inspect. Trythe above examples with to_s rather than inspect and see what happens.For everything including nil we see a string representation of that object. For the number 1 we see"1". For the string "1", we see the object again because its already a string. Converting an Arrayto a String gives us "[1,2,3]", which clearly shows us that the object is an array consisting of theelements 1, 2 and 3.Calling nil on the other hand, produces nothing; just an empty string (). Thats because nil isnothing. This whole explanation was to demonstrate that calling this code is not enough:

    Due to the debug method calling to_s rather than inspect on the objects it is passed. We must callinspect ourselves:

    This way, the debug method receives a String object already and will just output that.Now lets see this in action by firing up a new server process with this application:

    rails server

    Navigating to http://localhost:3000 will show just the New Post button. We know that the rootaction works because our test is not failing on that line. Clicking New Post now will show us theoutput of the debug call, which will be this:

    --- nil...

    We can see here that our @post variable is indeed nil, just like the error message said: Firstargument in form cannot contain nil or be empty. Now why is this?We know that the route is working correctly because were currently on the page at /posts/newlooking at the debug message we put there. The route routes to the controller, and the controller is

  • Debugging Rails 12

    empty. After the controller runs, the view template is rendered. Nowhere along the chain is the @postvariable defined for our form_for call in app/views/posts/new.html.erb and that is the cause ofthis bug.Where should we be defining this variable? Not in the view itself, because it is never theresponsibility of the view to collect data. That is the controllers job, and so the code to definethe @post variable should go in the controller. But where in the controller?A clue lies in the server output over in our console.

    Reading log outputWhen we made a request to /posts/new, it shows this:

    Started GET "/posts/new" for 127.0.0.1 at 2013-11-09 11:29:09 +1100Processing by PostsController#new as HTML

    Rendered posts/new.html.erb within layouts/application (0.6ms)Completed 200 OK in 4ms (Views: 3.6ms | ActiveRecord: 0.0ms)

    This text has a whole lot of information compressed into a little bit of space. Knowing how to readand interpret logs from Rails is an important skill, so lets go through the details of this now. On thefirst line we have the details about the request:

    Started GET "/posts/new" for 127.0.0.1 at 2013-11-09 11:29:09 +1100

    This shows us that we have made a GET request to the application, requesting the /posts/new path.The next two bits of information is the IP address of our local computer and the timestamp for therequest.On the second line we have this:

    Processing by PostsController#new as HTML

    This indicates to us that the route has beenmatched by Rails and has routed to the PostsControllersnew action. The request is a standard HTML format, meaning that HTML output will be returnedby this request.The third line is this:

    Rendered posts/new.html.erb within layouts/application (0.6ms)

    This tells us that the posts/new.html.erb template was rendered within layouts/application.This means that the controller has automatically chosen to display this template for this action, andhas used the default layout for the application to wrap around that template. All the rendering took0.6ms in this case.The fourth and final line shows this:

  • Debugging Rails 13

    Completed 200 OK in 4ms (Views: 3.6ms | ActiveRecord: 0.0ms)

    This line tells us that the response was completed successfully and returned a 200 OK response. Thisis the HTTP status part of the response which indicates to browsers the final status of their request.We can see that the request completed in 4ms total, with the views taking 3.6ms of that time andActiveRecord taking no time at all. The remaining 0.4ms were taken up by unknown things.

    Logs are written to filesIf you want to go back and see what happened after youve shut down the server, well beable to do that by viewing logs/development.log, which stores the exact same data thatis displayed when the server is running. The logs directory is where Rails writes its logdata to, and the filename is simply the environment that the Rails application is runningwithin. By default, that environment is development, so the log file that will be writtento will be logs/development.log.

    We know that we need to define the @post variable within the controller to make it available to theview, but where exactly? The logs tell us where exactly:

    Processing by PostsController#new as HTML

    This line from the logs is telling us that the new action within the PostsController is being runbefore the view is rendered. This would be a perfect place to set up the variable, so lets do that nowby defining this code:

    app/controllers/posts_controller.rb

    1 class PostsController < ApplicationController2 def new3 @post = Post.new4 end5 end

    Is this enough to fix our test? Lets find out by running bundle exec rspec spec/features/posts_-spec.rb.

  • Debugging Rails 14

    1) Posts can begin to create a new postFailure/Error: click_link 'New Post'NameError:

    uninitialized constant PostsController::Post# ./app/controllers/posts_controller.rb:3:in `new'# ./spec/features/posts_spec.rb:6:in `block (2 levels) in '

    Not quite! Were now seeing a new error which is a NameError exception. This time its happeningfrom line 3 of the PostsController, which is this line:

    @post = Post.new

    The error says uninitialized constant PostsController::Post, but on this line were not looking upthe PostsController::Post constant, we just want Post! So whats happening here? Why does itsay PostsController::Post and not just Post?

    Constant lookups in RubyWhen Ruby attempts to look up a constant, it will first attempt to look it up within the currentconstant context. Because were within the PostsController, it will attempt to look it up there. Itwill then travel up the hierarchy looking for that constant until it reaches the top-level namespace.If it cant find it there, then it gives up and shows an error message saying that it couldnt find it inthe current context. We can demonstrate this constant lookup using a very basic Ruby program:

    FOO = "foo"module Foo

    class Putterdef self.put

    puts FOOendendendFoo::Putter.put

    With this code, weve defined a FOO constant at the very top level. After that, weve defined a modulecalled Foo and a class called Putter, and that class has a method called put which calls puts FOO.This code will search up the hierarchy, looking for the constant in the Putter class, then the Foomodule and then finally the main namespace.Go ahead and put this code in a new file and try to run it. Well see it outputs foo. The constantlookup is working correctly.Now comment out the FOO constant and try running it again. Well see this happen:

  • Debugging Rails 15

    foo-putter.rb:5:in `put': uninitialized constant Foo::Putter::FOO (NameError)from foo-putter.rb:10:in `'

    Ruby is telling us here that it cannot find the constant any more, which is true because wecommented it out! The most important part of this error message is that it cant find the constantwithin the Foo::Putter namespace.Try now uncommenting FOO and moving the constant to inside the module Foo, like this:

    module FooFOO = "foo"class Putter

    def self.putputs FOOendendendFoo::Putter.put

    When we run this code again, well see that it works just the same as if the constant was defined inthe main namespace. The code will work also if the FOO constant is inside the class:

    module Fooclass Putter

    FOO = "foo"def self.put

    puts FOOendendendFoo::Putter.put

    This should demonstrate quite well how constant lookup works within Ruby. Lets go back to solvingour new problem, the uninitialized constant PostsController::Post message, armed now with ournew knowledge of constant lookup.

    Creating the Post modelWe need to define a Post constant within the application for our test to be happy. The best way todo this would be to generate a new Post model, which we can do with this command:

  • Debugging Rails 16

    rails g model post

    Along with this model comes a migration to create the table. If we attempt to view our applicationwithout running this migration, well see this error when we make a request to http://localhost:3000.

    Migrations are pending;run 'bin/rake db:migrate RAILS_ENV=development' to resolve this issue.

    We can fix this by running the command that it tells us to:

    bin/rake db:migrate RAILS_ENV=development

    Well see a similar errorwhenwe try to run our automated test, bundle exec rspec spec/features/posts_-spec.rb:

    ... Migrations are pending;run 'bin/rake db:migrate RAILS_ENV=test' to resolve this issue.

    We can run the recommended command here as well to fix the problem.

    bin/rake db:migrate RAILS_ENV=test

    When we run our test again, well see a different error:

    Failure/Error: click_link 'New Post'ActionView::Template::Error:

    undefined method `title' for ## ./app/views/posts/new.html.erb:4:...# ./app/views/posts/new.html.erb:1:...# ./spec/features/posts_spec.rb:6:...

    Weve now gotten past the error in our controller and now were back to an error withinapp/views/posts/new.html.erb. While line 1 of this file is mentioned in the stacktrace, it is notthe final line and therefore the error is probably not occurring on that line. The very first line of thestacktrace points to line 4, which is this line:

  • Debugging Rails 17

    The error were seeing is happening because somehow title is being called on an instance of thePost class. This is happening because the text_field helper, along with many other form helpersin Rails, will attempt to populate the form with the value from the attribute. It does this by trying tocall the attributes method. If the attributes method is not there, then we see this error happening.Whats happening here is that we didnt define any columns in the posts table, which means thatthere will be no title or body attribute defined for the form to use.We can fix this by altering the migration for the posts table, which is the only migration in thedb/migrate folder. Before we alter it, we need to undo the migration, which we can do by runningthis command:bin/rake db:rollback

    Now the database is back to its pristine state, we can alter that migration:class CreatePosts < ActiveRecord::Migration

    def changecreate_table :posts do |t|

    t.string :titlet.text :bodyt.timestampsendendend

    Now that the migration is correct, we can run this command to create the table with the correctcolumns:bin/rake db:migrate

    We need to do the same thing with the test database, and that can be done with this command:bin/rake db:test:prepare

    Lets run that test again and see if everythings running smoothly:1 example, 0 failures

    Yay! Our test is now passing. The router is receiving the request and passing it to the PostsController.The new action in that controller is defining a @post instance variable set to a new instance of the

    Post model. The app/views/posts/new.html.erb template is being run and rendering the form.The form is attempting to fetch the attributes from the new Postmodel instance, but since there arenone then the fields will be left blank. All the parts are working in unity and weve debugged thisbug.

    Table of ContentsDebuggingBasic Example #1Basic Example #2

    Debugging RailsWorkflowRails Example #1