Archive for January 2011

My previous blog post received some good comments. In this blog post I am going to further expand on what others are saying .i.e. using expanded path is a bad idea.

Ability to provide alternative implementation

Let's say I have two files.

  
    #main.rb
    require 'lib/util'

    #lib/util.rb
    puts "Hello World"
  

If you execute ruby main.rb you will see Hello World. No surprise there.

However let's say that I have another directory called common in my dev folder. In this dirctory create a directory called lib and then store a file named util.rb.

  
    #~/dev/common/lib/util.rb
    puts "Hello Common World"
  

Now let's say that for testing purpose I want main.rb to load util.rb from our common library. Here is how you have to do to ensure that util.rb is picked up from common directory.

  
    ruby -I/Users/nsingh/dev/common main.rb
  

This time the output I got was

  
    Hello Common World
  

Now let's change original main.rb to be use expanded_path. New code would look like this

  
    require File.expand_path("../lib/util", __FILE__)
  

Now let's try to use util.rb from common directory.

  
    ruby -I/Users/nsingh/dev/common main.rb
  

This time output will be NOT be what we saw last time.

  
    Hello World
  

This is exactly what Aaraon Patterson is talking about when he says if you use File.expand_path then others can't provide an alternative implementation.

In real life I have never done anything like providing alternative implemenation of an item. However I do understand that in theory it makes sense to not to use expanded path.

spec_helper

If you are testing using rspec then your specs will have require 'spec_helper' at the top of the file. If you want to run tests for an individual file then you can do

  
    ruby spec/models/user_spec.rb
  

Note that there are better ways to execute a particular rspec file. I am using the given case only to discuss about require using relative path vs require using absolute path.

However that code will fail since spec_helper is not in the path. One way to fix that would be to execute following command

  
    ruby -Ispec spec/models/user_spec.rb
  

Another way to fix it would be to change the user_spec.rb to require spec_heper using expanded path.

  
    require File.dirname(__FILE__) + '/../spec_helper'
  

Now you can run ruby spec/models/user_spec.rb and it will run fine. However declaring expanded path comes with a cost. Let's say that tomorrow I move user_spec.rb to spec/models/users directory. Now the expanded path is pointing to a wrong file. If I had used spec_helper.rb then this problem would have not been there.

Since usage of expanded path reduces the flexibility of providing alternative implementation and increases the cost of moving around files, I am changing my vote from using expanded path to not using expanded path.

requiring rubygems

Ryan Tomayko wrote about the need to not to require . Sintarta testing documentation follows that guidelines and does not require rubygems. Here is the issue. If a ruby newbie comes along and stores the test file in test.rb and executes the file with command ruby test.rb , then the user will get following error message

  
    `require': no such file to load 'sinatra'
  

Most likey user will do gem install sinatra and it will still not work. The user does not know that rubygems is not being loaded. The correct command would be

  
    ruby -rubygems test.rb
  

I like the approach taken by cucumber. If you look at cucumber/cli/main.rb you will see following code

  
    begin
      require 'gherkin'
    rescue LoadError
      require 'rubygems'
      require 'gherkin'
    end
  

The cucumber team is trying to not to require rubygems but in case rubygems is not already loaded then they load it for you.

I know this rubygems issue is not directly related to expanded path discussion but since it came in the discussion in the comments, I wanted to provide my point of view.

The require command is not suppossed to load a file that is already loaded. However while requiring a file, ruby VM looks at the path and the file name and it can cause some strange behavior.

Basics

Ruby allows two ways to load file: require and load. load will load a file every single time while require will not load already loaded file.

I have two files here in the same directory: main.rb and foo.rb . Here is what files look like.

  
    #main.rb
    require 'foo'
    require 'foo'
  
  
    #foo.rb
    puts "I am being loaded"
  

If you execute ruby main.rb you will see a single message "I am being loaded". That is because once a file is already loaded then it wil not be loaded second time.

Require using relative path

Now let's change the main.rb to require foo.rb using relative path. After the change file looks like this

  
    #main.rb
    require 'foo'
    require '../dev/foo'
  

If you execute ruby main.rb you will see the puts message twice. That is because ruby is loading foo.rb twice.

$LOADED_FEATURES - keeping track of what's loaded and what's not

ruby uses $LOADED_FEATURES , a global constant, to keep track of what's loaded and what's not loaded. Here is an updated version of main.rb.

  
    #main.rb
    require 'foo'
    require 'foo'
    puts $LOADED_FEATURES.inspect
    require '../dev/foo'
    puts $LOADED_FEATURES.inspect
  

If you execute ruby main.rb you will see following message.

  
    I am being loaded
    ["enumerator.so", "foo.rb"]
    I am being loaded
    ["enumerator.so", "foo.rb", "../dev/foo.rb"]
  

As you can see ruby while loading a file also stores the path that was provided. Now that we know that ruby looks at the path fix is easy. Here is fixed version.

  
    require File.expand_path('../foo', __FILE__)
    puts $LOADED_FEATURES.inspect
    require File.expand_path('../../lab/foo', __FILE__)
    puts $LOADED_FEATURES.inspect
  

If you execute ruby main.rb you will see following message.

  
    I am being loaded
    ["enumerator.so", "/Users/nsingh/dev/scratch/lab/foo.rb"]
    ["enumerator.so", "/Users/nsingh/dev/scratch/lab/foo.rb"]
  

As you can see foo.rb is being loaded only once and that is because $LOADED_FEATURES has the absolute path to foo.rb.

Summary

There is no need to have abosolute path in all the cases. However in a complex case where a number of different files are being required and some of the required files in turn require more files dynamically then it is best to have a policy of everyone requiring file using absolute path.

Recently in the Spree project, the tests started to fail with the message that factories are already loaded. This was because same factory was being loaded by two different gems using different relative paths. Once I converted the code to use absolute path this problem was fixed.