March 30, 2010

Posted by John

Tagged gems and testing

Older: A Nunemaker Joint

Newer: I Have No Talent (redux)

Because Gem Names Are Like Domains in the 90's

One of my favorite parts of every new gem is naming it. The other day, when I was trying to name joint, it occurred to me that I should always check if a gem name is available before I create my project. I did a quick search on RubyGems and discovered it was available.

Last night, I decided I should whip together a tiny gem that allows you to issue a whois command to see if a gem name is taken. Why leave the command line, eh?

Installation

gem install gemwhois

This adds the whois command to gem. Which means usage is pretty fun.

Usage

$ gem whois httparty

   gem name: httparty
     owners: John Nunemaker, Sandro Turriate
       info: Makes http fun! Also, makes consuming restful web services dead easy.
    version: 0.5.2
  downloads: 40714
  
$ gem whois somenonexistantgem

  Gem not found. It will be mine. Oh yes. It will be mine. *sinister laugh*

If the gem is found, you will see some details about the project (maybe you can convince them to hand over rights if they are squatting). If the gem is not found, you will receive a creepy message in the same vein as the RubyGems 404 page.

The Fun Parts

The fun part of this gem was recently I noticed that other gems have been adding commands to the gem command. I thought that was interesting so I did a bit of research. I knew that both gemedit and gemcutter added commands so I downloaded both from Github and began to peruse the source. Turns out it is quite easy.

First, you have to have a rubygems_plugin.rb file in your gems lib directory. This is mostly ripped from gemcutter:

if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.3.6')
  require File.join(File.dirname(__FILE__), 'gemwhois')
end

Next, you have to create a command. At the time of this post, here is the entirety of the whois command:

require 'rubygems/gemcutter_utilities'

class Gem::Commands::WhoisCommand < Gem::Command
  include Gem::GemcutterUtilities

  def description
    'Perform a whois lookup based on a gem name so you can see if it is available or not'
  end

  def arguments
    "GEM       name of gem"
  end

  def usage
    "#{program_name} GEM"
  end

  def initialize
    super 'whois', description
  end

  def execute
    whois get_one_gem_name
  end

  def whois(gem_name)
    response = rubygems_api_request(:get, "api/v1/gems/#{gem_name}.json") do |request|
      request.set_form_data("gem_name" => gem_name)
    end

    with_response(response) do |resp|
      json = Crack::JSON.parse(resp.body)
      puts <<-STR.unindent

        gem name: #{json['name']}
          owners: #{json['authors']}
            info: #{json['info']}
         version: #{json['version']}
       downloads: #{json['downloads']}

      STR
    end
  end

  def with_response(resp)
    case resp
    when Net::HTTPSuccess
      block_given? ? yield(resp) : say(resp.body)
    else
      if resp.body == 'This rubygem could not be found.'
        puts '','Gem not found. It will be mine. Oh yes. It will be mine. *sinister laugh*',''
      else
        say resp.body
      end
    end
  end
end

The important part is inheriting from Gem::Command. Be sure to require 'rubygems/command_manager' at some point as well. Once you have the rubygems_plugin file and a command created, you simple register the command:

Gem::CommandManager.instance.register_command(:whois)

The comments and code in RubyGems itself is pretty helpful if you are curious about what you can do.

Testing

The trickier part was testing the command. Obviously, building the gem from gemspec and installing over and over does not a happy tester make. I did a bit of research and found the following testing output helpers and the unindent gem:

module Helpers
  module Output
    def assert_output(expected, &block)
      keep_stdout do |stdout|
        block.call
        if expected.is_a?(Regexp)
          assert_match expected, stdout.string
        else
          assert_equal expected.to_s, stdout.string
        end
      end
    end

    def keep_stdout(&block)
      begin
        orig_stream, $stdout = $stdout, StringIO.new
        block.call($stdout)
      ensure
        s, $stdout = $stdout.string, orig_stream
        s
      end
    end
  end
end

With these little helpers, it was quite easy to setup the command and run it in an automated way:

require 'helper'

class TestGemwhois < Test::Unit::TestCase
  context 'Whois for found gem' do
    setup do
      @gem = 'httparty'
      stub_gem(@gem)
      @command = Gem::Commands::WhoisCommand.new
      @command.handle_options([@gem])
    end

    should "work" do
      output = <<-STR.unindent

        gem name: httparty
          owners: John Nunemaker, Sandro Turriate
            info: Makes http fun! Also, makes consuming restful web services dead easy.
         version: 0.5.2
       downloads: 40707

      STR
      assert_output(output) { @command.execute }
    end
  end
  
  context "Whois for missing gem" do
    setup do
      @gem = 'missing'
      stub_gem(@gem, :status => ["404", "Not Found"])
      @command = Gem::Commands::WhoisCommand.new
      @command.handle_options([@gem])
    end

    should "work" do
      output = <<-STR.unindent

        Gem not found. It will be mine. Oh yes. It will be mine. *sinister laugh*

      STR
      assert_output(output) { @command.execute }
    end
  end
end

The only other piece of the puzzle was using FakeWeb to stub the http responses for the found and missing gems. You can see more on that in the test helper file.

Conclusion

At any rate, the gem is pretty tiny and possibly useless to others, but it was fun. Gave me a chance to play around with testing STDOUT and creating RubyGem commands. Plus, now I know if the gem name I want is available in just a few keystrokes.

18 Comments

  1. Creepy? You need to watch Wayne’s World again. :)

  2. Exsqueeze me? Baking powder?

  3. Nice. I like it.

  4. Instead of reading “sinister laugh” it might get the point across better to hear it:

    system("say -v hysterical Muahahahahaha")

    The deranged voice works well too. OSX only of course…

  5. @Nick: Ha. I guess I do.

    @Zef: Hahahahahaa. That is epic. I might have to add that.

  6. This will be really useful as more and more gems are released…

    @Zef. LOL! That’s hilarious. :D

  7. You may be interested in this post which shows how to do a whois of multiple gems with results displayed in a table.

  8. Just a note, the gem didn’t work with rubygems 1.3.5. Updating to 1.3.6 fixed it.

  9. Confirming that it doesn’t work with RubyGems 1.3.5. You get this:

    ERROR:  While executing gem ... (RuntimeError)
        Unknown command whois
  10. Yes, it doesn’t work on 1.3.5. I am aware. I have a 1.3.6 requirement on it (as does gemcutter).

  11. I would rather use this:

    say -v Deranged Muahahahahahahahaha

  12. This is really useful, thanks!

  13. You may want to change how you set up your plugin command.

    the lib/rubygems_plugin.rb file will be loaded from your gem when rubygems is required(code). This means that your gem will always be required when rubygems is.
    $ gem install gemwhois
    $ irb
    >> require ‘rubygems’
    => false
    >> Gem::Commands::WhoisCommand
    => Gem::Commands::WhoisCommand

    The other problem is that since Gem.find_files grabs all matching files from all gem versions in the gem path, all installed versions of the gem will have their rubygems_plugin code loaded.

    The way to resolve this is to limit lib/rubygems_plugin.rb’s content to simply

    Gem::CommandManager.instance.register_command(:whois)

    And to move your command code to
    /lib/rubygems/whois_command.rb
    where rubygems CommandManager will pick it up(code)

  14. @Nick Howard: Hmm. I was pretty much just copying gemcutter which I assumed would be doing things the proper way. I’ll take a look at your suggestions. Looks interesting.

  15. I first ran into this behavior with YARD, which did something similar, only it hooked into the rdoc command. I thought it was a bug at first, but it’s documented in rubygems rdocs.

    Oh, and my links(where I said code):
    http://github.com/jbarnette/rubygems/blob/master/lib/rubygems/command_manager.rb#L170

    http://github.com/jbarnette/rubygems/blob/master/lib/rubygems.rb#L1107

  16. @Nick Howard: Updated with 0.2.1.

  17. The nasty fact about screwing up a rubygems_plugin.rb file is that you can’t fix it in another release. You have to tell people to uninstall the old one.

    This is why Carlhuda had to tell people to uninstall Bundler 0.8 and why YARD hooks will continue to suck even after the maintainers fix them.

  18. @Mislav: Yep. Thankfully, this was caught early.

Sorry, comments are closed for this article to ease the burden of pruning spam.

About

Authored by John Nunemaker (Noo-neh-maker), a programmer who has fallen deeply in love with Ruby. Learn More.

Projects

Flipper
Release your software more often with fewer problems.
Flip your features.