Entries Tagged 'Ruby' ↓

Writing a Simple Rake Task - Part 1

I’ve got a bunch of little tasks that we do at work that we run through cron, and most of them involve our rails application.  We’ve run a lot of them through script/runner, which is a fine tool in its own right, but after doing a little research, I think turning our simple Ruby tasks into rake tasks makes more sense for a few reasons.  First it might be easier to grok for someone coming up to speed on our code if we can just tell them that once a week we run a rake task that sends out a report email to our sales guys, instead of having to explain that we run a Ruby script through script/runner to load our Rails environment, and possibly have to explain what script/runner even is (a lot of candidates we talk to haven’t really explored the Rails skeleton that you get with any new project… makes me kind of sad).  Second, if you forget what it should be, you can easily do a search with rake -T.

So, what I’m going to do here is show you how to write a simple rake task for deleting a plugin (script/plugin doesn’t have an uninstall option, which I find strange).  You could probably just extend script/plugin for this (I know you could… Rails is all about extensibility), but it’s a simple example with an actual useful bit of functionality so it will let you see how things evolve.  I’m a bit wordy, so this is going to be broken into two posts for convenience sake.

First, let’s start with an empty rails project.  So, go to wherever you keep your one-off code, and type:

$ rails rake_test_example
$ cd rake_test_example

Alright, so, I’m assuming some familiarity with Rails, rake, and Ruby here, so if you don’t know what’s going on at this point, you probably should start somewhere else.  If you’re still here, I’m assuming you’re ready for the straight-talk.  So, when writing your own rake task, the first thing you should know is where they go.  In your Rails application, there is a directory called “lib”, and inside of lib is another directory called “tasks”.  This is where you put your custom rake tasks.  Any files you put in here called “some_file_name”.rake will be picked up by the rake executable, and including in the list of available tasks.  How does rake do this?  Glad you asked.   In your Rails install directory, you can find the salient code in “railties/lib/tasks/rails.rb”, and the line that actually does it is:

Dir["#{RAILS_ROOT}/vendor/plugins/*/**/tasks/**/*.rake"].sort.each /
{ |ext| load ext }

See, no magic, just some Ruby code. Anyway, glad you’re curious, but let’s move on.  So, when you’re writing your own plugin, the first thing to remember is that you put it in “lib/tasks/” and the filename needs to have the “.rake” extension.  What goes in a rake file you ask?  Well, let’s continue.  Create a new file “lib/tasks/example.rake” and open it in a text editor.  Type in the following:

task :hello_rake do
puts "Hello, Rake"
end

Save the file and let’s run our rake task now.  At the command line run

$ rake hello_rake
(in .../projects/code/rails/engines_demo)
Hello, Rake

And voila, you now have written your first rake task.  If you now tell rake to list its tasks though, you won’t find your entry in the list.  Searching for it won’t yield you any more luck either.  (Give it a shot here)

$ rake --tasks
$ rake -T hello_rake

No results. All we need to do is add one line to our custom task and we’re good to go.

desc "This is our hello world task"
task :hello_rake do
puts "Hello, Rake"
end

I personally find this a little unintuitive (opposed to putting the desc block inside the task you’re documenting), but if you think of it as more of an annotation it sort of makes sense. Anyway, if we run our command to search for the task, we now have an entry for our custom task

$ rake -T hello_rake
(in .../projects/code/rails/engines_demo)
rake hello_rake  # This is our hello world task

Another thing that you often see in rake tasks is namespacing.  For instance, you see a lot of tasks like db:migrate, db:rollback, spec:models, etc.  Namespacing is a good way of keeping things neat so that you can hierarchically organize similar tasks.  Adding namespacing to our custom tasks is almost as easy as adding our documentation for the task.  Open up example.rake and change the code like so:

namespace :custom do
desc "This is our hello world task"
task :hello_rake do
puts "Hello, Rake"
end
end

After we save that and run our command to search for the task, we now get this:

$ rake -T hello_rake
(in .../projects/code/rails/engines_demo)
rake custom:hello_rake  # This is our hello world task

Ok, so now that we’ve got the basics of rake down, let’s move on to doing something a little bit more useful.  script/plugin is a great tool for grabbing and installing plugins from just about anywhere, but it doesn’t have an obvious way of deleting them (that I’ve found yet.  Someone prove me wrong, please!).  We could just modify script/plugin ourselves, but instead we’re going to write our own custom rake task to remove a plugin.  There’s not much to it, but we’re going to expand it a little bit so that if the plugin directory is under source control through SVN or Git we’ll remove it from our repositories before we delete the actual files, so they don’t hang around.  Starting at the top, we’re going to create a task called “remove and put it in a namespace called “plugins”.  So let’s open an editor, and create a file in lib/tasks called plugin.rake.  Inside plugin.rake we’re going to then add these lines first:

namespace :plugins do
desc "Removes an unwanted plugin"
task :remove do
end
end

After we save, let’s run the rake command to search for our new task to make sure it’s actually there.  As an aside, I’m going to be doing this in very small, iterative steps with a lot of testing in between.  I don’t know how you might test-drive a rake task, but if anyone has any information on it I would really appreciate it.  Since I don’t posses it right now though, baby steps!

$ rake -T plugins:remove
(in .../projects/code/rails/engines_demo)
rake plugins:remove  # Removes an unwanted plugin

Success!  Now, the next thing we’re going to want to do is to make sure the user provides an argument that is the name of the plugin we’re looking for.  To do that, rake adds any key-value pairs passed in on the command line to the ENV array that you get for free.  We’re going to follow Occams Razor here and name our parameter for the plugins name that we want to remove “name”.  This is how it would look on the command-line.

$rake plugins:remove name=our_great_plugin

Let’s open up our plugin.rake file and add a little bit of code to check if it exists, and if not prints out an error to the user.  If it does, let’s confirm what the parameter is:

namespace :plugins do
desc "Removes an unwanted plugin"
task :remove do
unless plugin_name = ENV["name"]
puts “You must provide a plugin name”
else
puts “plugin_name = #{plugin_name}”
end
end
end

We now have two functional scripts to run to test our script (with and without a ‘name=’ parameter).  Let’s verify our results:

$ rake plugins:remove
(in .../projects/code/rails/engines_demo)
You must provide a plugin name

$ rake plugins:remove name=demo_plugin
(in .../projects/code/rails/engines_demo)
plugin_name = demo_plugin

Success!  Tomorrow we’ll continue with this and add the functionality to actually remove the plugin.

Proc#arity strangeness

I was fooling around with some examples using blocks this morning before work, and I noticed that the ruby docs for Proc#arity say:

Proc.new {}.arity          #=>  0
Proc.new {||}.arity        #=>  0
Proc.new {|a|}.arity       #=>  1
Proc.new {|a,b|}.arity     #=>  2
Proc.new {|a,b,c|}.arity   #=>  3
Proc.new {|*a|}.arity      #=> -1
Proc.new {|a,*b|}.arity    #=> -2

But the ruby interpreter I’m using (MRI 1.8.6) actually returns these results:

Proc.new {}.arity          #=>  -1
Proc.new {||}.arity        #=>  0
Proc.new {|a|}.arity       #=>  1
Proc.new {|a,b|}.arity     #=>  2
Proc.new {|a,b,c|}.arity   #=>  3
Proc.new {|*a|}.arity      #=> -1
Proc.new {|a,*b|}.arity    #=> -2

Anyone know why the empty block returns -1 for its arity? Is it a known bug, or is it supposed to work like that and the documentation is wrong? In this case I think the documentation makes a lot more sense.

*** This appears to have been fixed in MRI 1.9 ***

Testing Restful Routes from the Console

While working on a problem recently, we realized that it would be extremely helpful to be able to test some of our restful routes via the console. So after a bit of hunting we found an excerpt from The Rails Way that did exactly what we needed it to. A few simple steps and you’ll be ready to go in no time. FYI, the following is tutorial-lite and assumes you already have a grasp of some of the basics of rails routing functionality. Onward…

Let’s create a simple project that we can use for demonstration purposes:

#: rails restful_routes
#: cd restful_routes

If you want to from here, fire up your rails app and make sure everything is kosher.

#: ruby script/server

(You can, of course, navigate from here to http://localhost:3000/ and get the rails start page if everything is good).

Alright, from here we’re going to start adding our RESTful routes to our rails application by editing config/routes.rb

Opening routes.rb let’s navigate down to the block below:

# Sample resource route (maps HTTP verbs to controller actions automatically):
# map.resources :products

Let’s insert the following simple route right underneath the previous code:

map.resources :users

Save the file and let’s fire up the console:

#: ruby script/console

To test our route on the command line, the first thing we need to do is include the ActionController::UrlWriter module:

>> include ActionController::UrlWriter

And from here all of our route helpers (*_path and *_url) are available from the console.

>> users_path
=> “/users”
>> new_user_path
=> “/user/new”
>> edit_user_path(:id => 1)
=> `”/user/1/edit”

Notice here that even though we don’t have a user with an id of 1 (or a User model, controller, or anything user related), Rails will still happily generate a route for us. All of the route generation faculties available from within rails are now available for you to test on the console, which will hopefully save you some of the hair we lost during our debugging session.

RSpec, and when it loads (objects, modules, etc)

Lately I’ve been playing around with RSpec, which is a fantastic testing tool that falls under the umbrella of what’s being called “Behavior Driven Development” (BDD).  What BDD advocates is that there should be a common language used to talk about your system, and that describing the behaviors of the system ensures that you are delivering what’s really valuable, instead of writing lots of code that may not be necessary.  If you are interested in learning more about BDD, I suggest you head over here and start reading, as it seems to be the resource to go to for more information. Anyway, I’m not here to talk about some quirks with RSpec in the hope that someone will stumble over here and provide some insight. Let’s start with some code and work from there towards my conclusions:

Here is a simple class representing a digital photo that you might store on a server, your computer, wherever. This is as simplified as I want it to be for the purposes of illustrating my points.

#!/usr/bin/ruby

class Photo
  attr_reader :height, :width

  def initialize( width, height )
    @width = width
    @height = height
  end

  def scale_absolute( width, height )
    @width = width
    @height = height
  end

  def scale_percentage( width, height )
    @width *= ( width / 100.0 )
    @height *= ( height / 100.0 )
  end
end

Our second code file is just a module of helper methods that I’ve creatively titled HelperMethods

module HelperMethods
  def nice_photo_resolution( photo )
  if photo.respond_to?( :width ) && photo.respond_to?( :height )
  return "#{photo.width}x#{photo.height}"
  end
  end
end

And here is our rspec code for testing the Photo class. We just simply use the describe method to create an ExampleGroup instance, setup some basic values, have a before method to instantiate an instance of a Photo, and then our it methods which create Example instances to be run.

#!/usr/bin/ruby

require 'photo'
require 'helper_methods'

describe Photo do
  include HelperMethods

START_HEIGHT = rand(640)
  START_WIDTH = rand(480)

before( :each ) do
  @photo = Photo.new( START_WIDTH, START_HEIGHT )
  end

it "should resize a photo from #{START_WIDTH}x#{START_HEIGHT}to 640x480" do
  @photo.scale_absolute( 640, 480 )
  @photo.width.should == 640
  @photo.height.should == 480
  end

it "should scale a photo 200% from #{START_WIDTH}x#{START_HEIGHT} to #{START_WIDTH * 2}x#{START_HEIGHT * 2}" do
  @photo.scale_percentage( 200, 200 )
  @photo.width.should == ( START_WIDTH * 2 )
  @photo.height.should == ( START_HEIGHT * 2 )
  end

it "should scale a photo 50% from #{START_WIDTH}x#{START_HEIGHT} to #{( START_WIDTH * 0.50 )}x#{( START_HEIGHT * 0.50)}" do
  @photo.scale_percentage( 50, 50 )
  @photo.width.should == ( START_WIDTH * 0.50 )
  @photo.height.should == ( START_HEIGHT * 0.50 )
  end

it "should rotate a photo 90 degrees such that the dimensions change from #{START_WIDTH}x#{START_HEIGHT} to #{START_HEIGHT}x#{START_WIDTH}" do
  @photo.rotate( 90 )
  @photo.width.should == START_HEIGHT
  @photo.height.should == START_WIDTH
  end
end

Now, as I’ve been playing around, I’ve noticed several things that have stuck in my craw. The first is that even though I’m including HelperMethods in the describe block, if I try to use it in the description argument passed to the it method, I get an error trying to run my spec test telling me that no such method exists. Take a look here to see what I mean:

describe Photo do
  include HelperMethods

START_HEIGHT = rand(640)
  START_WIDTH = rand(480)

before( :each ) do
  @photo = Photo.new( START_WIDTH, START_HEIGHT )
  end

it "should resize a photo from #{nice_photo_resolution( @photo )}to 640x480" do
  @photo.scale_absolute( 640, 480 )
  @photo.width.should == 640
  @photo.height.should == 480
  end

...
end

When I try to run the spec command on that ExampleGroup, I get an error saying the nice_photo_resolution method cannot be found. This leads me to believe RSpec is doing something with the it method before it builds the ExampleGroup instance from the describe method. Further supporting this theory is that when you try to instantiate instance level variables in a before method, they cannot be seen and are evaluated as nil in the code as its executed. Take a look here for an example:

describe Photo do
  include HelperMethods

before( :each ) do
  @start_width = rand( 640 )
  @start_height = rand( 480 )

@photo = Photo.new( @start_width, @start_height )
  end

it "should resize a photo from #{@start_width}x#{@start_height}to 640x480" do
  @photo.scale_absolute( 640, 480 )
  @photo.width.should == 640
  @photo.height.should == 480
  end
...
end

That example is a little contrived, but I think it gets the point across. Moving to a more realistic example, this produces the same error:

describe Photo do
  include HelperMethods

before( :each ) do
  @photo = Photo.new( rand( 640 ), rand( 480 ) )
  end

it "should resize a photo from #{@photo.width}x#{@photo.height}to 640x480" do
  @photo.scale_absolute( 640, 480 )
  @photo.width.should == 640
  @photo.height.should == 480
  end
...
end

This will cause the spec command to throw an error, because @photo is evaluating to nil, and trying to access the width or height instance fields of the object cause an error to be thrown.

I’m not entirely sure that my imperfect understanding of Ruby class instantiation is at fault for my confusion, but it doesn’t seem like it should be the case. I also haven’t cracked open the RSpec code yet, as I am just trying to learn the system now and don’t want to have to dive into the gory details until I understand how to use it to get work done. If anyone can point me in the right direction though I would truly appreciate it. These examples are fairly unrealistic, but I think it’s a short jump to seeing how not understanding the order of construction could lead to some problems when working on production-level code. I’d rather be testing my code than testing the RSpec system.