Thursday, March 20, 2008

Playing with Signals in Ruby

What?

Signal trapping and processing in Ruby - it's way easy.

Why?

Signals are very useful, e.g. to detect shutdown and let your program clean up after itself.

How?

Here's a quickie that'll get you started playing.



# Let's see which signals we can trap

{
"ABRT"=>6, "ALRM"=>14, "BUS"=>7,
"CHLD"=>17, "CLD"=>17, "CONT"=>18,
"FPE"=>8, "HUP"=>1, "ILL"=>4,
"INT"=>2, "IO"=>29, "IOT"=>6,
"KILL"=>9, "PIPE"=>13, "PROF"=>27,
"QUIT"=>3, "SEGV"=>11, "STOP"=>19,
"SYS"=>31, "TERM"=>15, "TRAP"=>5,
"TSTP"=>20, "TTIN"=>21, "TTOU"=>22,
"URG"=>23, "USR1"=>10, "USR2"=>12,
"WINCH"=>28, "XCPU"=>24, "XFSZ"=>25
}.each do |signal,num|
puts "Set up trap for #{signal} #{num}"
trap signal do
puts "#{signal} #{num} was trapped"
end
end

# Sleep away and lets press some buttons. Try for example ctr-c
sleep 100


Also check out:


at_exit do
puts "I'm quitting now!"
end


Very useful!

Saturday, March 15, 2008

Behavior Driven Development in Rails - Easy and Fun!

What?

Behavior driven coding for Ruby, and especially, Ruby on Rails.

This is a summary and run-through of the excellent RailsEnvy Love Tests presentation.

Why?

  • Tests increase your confidence, and let you sleep at night.
  • Writing tests before writing you code makes you write better code.
  • Writing the tests and specifications first, you will know when you're done - all the tests pass!
  • Tests make for excellent documentation, in the sense that Examples are more powerful than Descriptions

Also - it's fun! Do it Red -> Green -> Refactor style with RSpec and Auto testing Zen.

How?

Let's consider the example of a user who has forgotten her email.

Writing good behavior specifications is like telling a short story - a good story defines your app behavior:

A user who has forgotten a password
- Should be able to enter and submit their email address
- Should be sent an email if their email address is valid
- Should be shown a notice if the email address is invalid
- Should be redirected to the login screen if their email is valid
- Should be shown an error message if the email address is not in the system, and be shown the form again.

We use RSpec to turn our story into action.

Install RSpec

Without rails (just the gem)

gem install rspec

For rails, with its beautiful helpers

ruby script/plugin install svn://rubyforge.org/var/svn/rspec/tags/CURRENT/rspec
ruby script/plugin install svn://rubyforge.org/var/svn/rspec/tags/CURRENT/rspec_on_rails

./script/generate rspec

To run your tests, you just do

rake rspec

OR you turn testing into an automated game. ZenTest will run tests *every time your files change* and pop up notifications for passes/fails using Growl/Snarl.

gem install ZenTest redgreen
autotest

Coding the behavior

Now actually write the RSpec code - or just flow by turning your English into RSpec with the awesome textmate YAML to RSpec plugin. Direction for easy install.

On to the testing. It is important to keep the component we're testing separated from the rest of the system. E.g. if we want to test the controller of a user retrieving a lost password, we don't want it to be dependent on the Model part working correctly at test times.

Enter the stage: Stubs and Mocks.

You use these to manage "canned" answers. E.g. if you want to test for a user with the name Greg who is in the system and entered his email correctly, you do


@user = mock_model(User).stub!(:first_name).returns("Greg") # Let there be a Greg
@user = mock_model(User, :first_name => "Greg") # Briefer syntax
User.stub!(:find_by_email).returns(@user) # Always find Greg

What to test?

Every line of code you write. Seriously. In Rails you write so little code that this should be possible. However, make sure that you don't slip into testing Rails - it is already rather well tested!

Examples

Validation


validates_presence_of :first_name
# To test it =>
@user.should_have_at_least(1).errors_on(:first_name)


Object relation mapping


has_many :user_roles
# Test
@user.user_roles.create(:role => "Admin")
@user.user_roles.count.should == 1


UI Redirection


redirect_to :action => 'index'
# Test
response.should redirect_to(:action => "index")


Do it

Read the RSpec website - it reads like a good book
Come up with and start a New project
Keep 100% Test coverage, or at least attempt it!
Feel good about yourself
Make sure tests pass

Friday, March 14, 2008

Publish Ruby gems to RubyForge in 10 minutes

What?

A quick tutorial to how to publish your ruby gems to the rubyforge gem listing in just under 10 minutes.

This is a fat-reduced version of nuby on rails' tutorial. Thanks for the great info!

Why?

You want to share your code, right? Sharing your ruby code is *really easy*!

How?

Using hoe. This assumes you already have a rubyforge account and a project approved. See https://rubyforge.org/account/register.php to set up an account and http://rubyforge.org/register/projectinfo.php to register a project.

Setup


sudo gem install hoe --include-dependencies

Set your environment EDITOR variable, if you haven't already. Edit ~/.profile or ~/.bash_login or equivelant, and put in

# ~/.bash_login
export EDITOR=/usr/bin/mate -w

Set up your computer to be able to talk to rubyforge. This will open a configuration file in your editor. Change the username and email

rubyforge setup
Log in to rubyforge

rubyforge login
Sow your project

For a project registered with rubygorge as object-stash you would do:

sow object-stash

This creates a directory object-stash with the files:

History.txt
Manifest.txt
Rakefile
README.txt
bin
sparklines
lib
sparklines.rb
test
test_sparklines.rb

It will list in the command line what you need to fix in the auto generated files.

*Note* If your project name contains a dash - you will have to fix the the class name from "Object-stash" to ObjectStash in lib/object-stash.rb and Rakefile. A shortcoming of hoe, but a minor one.

If you want to auto-publish your documentation to the project main site, e.g. http://object-stash.rubyforge.org/, then edit Rakefile and add:


p.remote_rdoc_dir = '' # Release to root

Write your code

Add and edit files in lib

Release

Run


rake check_manifest


to make sure your Manifest file is correct. The lines with a - are listed in the Manifest but don’t exist on disk. The lines with a + are not in the Manifest but are on the disk.

*Note* Make sure the file ends with at least one blank line!

Now edit History.txt

== 1.0.1

* New version
* Features


Update the version number in your ruby class files:

# lib/object-stash.rb
class ObjectStash
VERSION = '1.0.1'
end
Release!


rake release VERSION=1.0.1

Publish your docs!

rake publish_docs

View docs and install your gem remotely

Go to your project's base url to view the docs, e.g. http://object-stash.rubyforge.org/.

Install your own gem! As easy as 1, 2,

sudo gem install object-stash

Save Ruby objects to disk for later reload

What?

A way to store entire Ruby objects to disk.

Why?

Let's say you have a data structure that takes a long time to build, e.g. dictionary trie for the English language.

It wold be nice to be able to compute this data structure only once, then store it in some way for later retrieval.

How?

Using Ruby's standard library Marshal class, we can easily serialize an object and write it to disk. Combined with the gzip library we even have efficient storage.

Here's the result:


require 'zlib'

# Save any ruby object to disk!
# Objects are stored as gzipped marshal dumps.
#
# Example
#
# # First ruby process
# hash = {1 => "Entry1", 2 => "Entry2"}
# ObjectStash.sotre hash, 'hash.stash'
#
# ...
#
# # Another ruby process - same_hash will be identical to the original hash
# same_hash = ObjectStash.load 'hash.stash'
class ObjectStash

# Store an object as a gzipped file to disk
#
# Example
#
# hash = {1 => "Entry1", 2 => "Entry2"}
# ObjectStore.store hash, 'hash.stash.gz'
# ObjectStore.store hash, 'hash.stash', :gzip => false
def self.store obj, file_name, options={}
marshal_dump = Marshal.dump(obj)
file = File.new(file_name,'w')
file = Zlib::GzipWriter.new(file) unless options[:gzip] == false
file.write marshal_dump
file.close
return obj
end

# Read a marshal dump from file and load it as an object
#
# Example
#
# hash = ObjectStore.get 'hash.dump.gz'
# hash_no_gzip = ObjectStore.get 'hash.dump.gz'
def self.load file_name
begin
file = Zlib::GzipReader.open(file_name)
rescue Zlib::GzipFile::Error
file = File.open(file_name, 'r')
ensure
obj = Marshal.load file.read
file.close
return obj
end
end
end

if $0 == __FILE__
require 'test/unit'
class TestObjectStash < Test::Unit::TestCase
@@tmp = '/tmp/TestObjectStash.stash'
def test_hash_store_load
hash1 = {:test=>'test'}
ObjectStash.store hash1, @@tmp
hash2 = ObjectStash.load @@tmp
assert hash1 == hash2
end
def test_hash_store_load_no_gzip
hash1 = {:test=>'test'}
ObjectStash.store hash1, @@tmp, :gzip => false
hash2 = ObjectStash.load @@tmp
assert hash1 == hash2
end
def test_self_stash
ObjectStash.store ObjectStash, @@tmp
assert ObjectStash == ObjectStash.load(@@tmp)
end
def test_self_stash_no_gzip
ObjectStash.store ObjectStash, @@tmp, :gzip => false
assert ObjectStash == ObjectStash.load(@@tmp)
end
end
end




Here's an example:

# Here's our theory of everything - make it heavy
theory_of_everything = { :question => "Life, Universe & Everything", :answer => 42 }
1000.times {|i| theory_of_everything[i] = "rubbish#{i}" }

# Stash it
ObjectStash.store theory_of_everything, './theory_of_everything.stash'

# Let's see what it did
File.stat('./theory_of_everything.stash').size # => 4206

# Reload it
loaded_theory_of_everything = ObjectStash.load 'theory_of_everything.stash'
question = loaded_theory_of_everything[:question]
answer = loaded_theory_of_everything[:answer]
puts "The answer to #{question} is #{answer}" # => The answer to Life, Universe & Everything is 42


And for the curious - I wrote up a series of tests to make sure I grok how/that this works as I expect it to:


def report msg
puts msg
t=Time.now
yield
puts " -> #{Time.now-t}s"
end

def test_hash_equality hash1, hash2
throw :HashesNotEqual unless hash1 == hash2
throw :HashesNotTreeEqual unless hash1 === hash2
hash1.each do |key,value|
throw :HashCopyDoesNotHaveKey unless hash2.has_key? key
throw :HashCopyDoesNotHaveValue unless hash2.has_value? value
end
end

original_hash = Hash.new
loaded_hash = nil
marshal_dump = nil

report "Build a big hash to be marshalled" do
(0..1000).each do |i|
original_hash[i] = "Entry #{i}"
end
end

report "Store the hash" do
marshal_dump = Marshal.dump(original_hash)
end

report "Load a hash from the dump" do
loaded_hash = Marshal.load(marshal_dump)
end

report "Assert hashes are equal" do
test_hash_equality original_hash, loaded_hash
end

report "Write the marshal dump to file" do
file_out = File.new("marshal_test.tmp",'w')
file_out.write(marshal_dump)
file_out.close
end

report "Construct hash from the marshal file dump" do
file_in = File.new('marshal_test.tmp','r')
loaded_hash = Marshal.load(file_in.read)
file_in.close
end

report "Assert hashes are equal" do
test_hash_equality original_hash, loaded_hash
end

require 'zlib'
report "Gzip the marshaled dump" do
file = File.new('marshal_test.tmp.gz','w')
gz = Zlib::GzipWriter.new(file)
gz.write marshal_dump
gz.close
end

report "Gunzip the marshaled dump and load it" do
gz = Zlib::GzipReader.open('marshal_test.tmp.gz')
loaded_hash = Marshal.load gz.read
gz.close
end

report "Assert hashes are equal" do
test_hash_equality original_hash, loaded_hash
end

puts "Success!"

Wednesday, March 5, 2008

iLogin - skip login forms on the iPhone!

"Logs you in rapidly (even with your iPhone)"

What is it?


Use it to log in to any website with a single click.


The first time, you click iLogin and fill in your info.


Every other time, you just click.


"Installation"


Drag this iLogin bookmarklet link to your toolbar (or right click and save to favorites).

Then just click it.

(If you ever enter the wrong login info, or just want to clear it, use the iLoginEraser.)


iPhone


Use Safari to save it as a bookmark to you Bookmarks folder (not the toolbar).

Then sync with your iPhone.


Open Source



Bookmarklet Composers


See the BookMarkLet-R to easily turn your javascript source into bookmarklets.

Tuesday, March 4, 2008

UoC Quickie3 - Now tested for iPhone

I've finally gotten the UoC login bookmarklet (post) to work reliably on the iPhone.

Ge it by saving this UoC Quickie3 link as a bookmark, either by dragging it to your toolbar or right-clicking and saving as a bookmark/favorite.

Two notes of caution: If you enter your information incorrectly the first time you will have to delete your cookies manually to get it to work again. The user name and password is saved scrambled, but in decodable plaintext. As always, protect your cookies.

Html-R: Make our Ruby HTML Pretty

What?

Html-R, an ajaxified online Ruby2HTML converter.

Why?

When you share your code online, it should look good and be readable, just like in your favorite text editor (emacs, right?).

With TextMate inspired syntax highlighting, Html-R quickly turns this



# Why's (Poignant) Guide to Ruby - The Endertrombe Wishmaker
require 'endertromb'
class WishMaker
def initialize
@energy = rand( 6 )
end
def grant( wish )
if wish.length > 10 or wish.include? ' '
raise ArgumentError, "Bad wish."
end
if @energy.zero?
raise Exception, "No energy left."
end
@energy -= 1
Endertromb::make( wish )
end
end
into that

# Why's (Poignant) Guide to Ruby - The Endertrombe Wishmaker
require 'endertromb'
class WishMaker
def initialize
@energy = rand( 6 )
end
def grant( wish )
if wish.length &gt; 10 or wish.include? ' '
raise ArgumentError, "Bad wish."
end
if @energy.zero?
raise Exception, "No energy left."
end
@energy -= 1
Endertromb::make( wish )
end
end

How?

Using jQuery, there's a listener that sits and waits for changes to the source code. After new code appears, it sends a re quest to the server which uses the syntax gem to generate the appropriate html.

The appended css is of my own design, and is intended to mimic that of TextMate.

Here are screenshots of the step by step process. Or you can just demo it yourself.




Monday, March 3, 2008

Has_and_belongs_to_many Sqlite3 headache: "SQL logic error or missing database"

What?

How to get many to many relations working quickly with Rails 2.0.2 and sqlite3 3.4.0.

Why?

Many to Many database relations between two models (tables) requires a middle table which records the relation. This can be difficult to get right, but Rails has some awesome helper functions to get you up and running quickly.

... when it works.

When it doesn't, you're likely to see the following:

"SQL logic error or missing database"
Which is about as descriptive as "it's not working." The kink is that Active Record (rails' database interface) will try to set the id field of the relational table, which is reserved by default as the primary key and therefore cannot be set to the same value twice (which rails will do).

How?

First create the models

./script/generate scaffold Actor first_name:string last_name:string
./script/generate scaffold Movie name:string

Then create a migration to set up the relational table:

./script/generate migration AddActorsMoviesRelation

Edit ./db/migration/xyz_AddActorsMoviesRelation.rb:
(Thanks EyeDeal for the :id => false tip!)


class AddActorsMoviesRelation < ActiveRecord::Migration
def self.up
create_table :actors_movies, :id => false do |t|
t.integer :actor_id, :foreign_key => true
t.integer :movie_id, :foreign_key => true
end
end

def self.down
drop_table :actors_movies
end
end



Then build the database

rake db:migrate

And finally edit ./app/models/actor.rb and ./app/models/movie.rb:


class Actor < ActiveRecord::Base
has_and_belongs_to_many :movies
end

class Movie < ActiveRecord::Base
has_and_belongs_to_many :movies
end



You should now be able to add movies to actors and vice versa with a simple


crime_fiction = Movie.create :name => "Crime Fiction"
san_fransisco = Movie.create :name => "The Streets of San Francisco"
jon = Actor.create :first_name => 'Jonathan', :last_name => 'Elliot'
jesse = Actor.create :first_name => 'Jesse', :last_name => 'Friedman'
dan = Actor.create :first_name => 'Dan', :last_name => 'Bakkedahl'

crime_fiction.actors << jon
jesse.movies << crime_fiction
dan.movies << crime_fiction

crime_fiction.actors.each {|actor| puts "#{actor.last_name}, #{actor.first_name}" }
# => Elliot, Jonathan
# => Friedman, Jesse
# => Bakkedahl, Dan

san_fransisco.actors << jesse
jesse.movies.each {|movie| puts movie.name }
# => Crime Fiction
# => The Streets of San Francisco

Sunday, March 2, 2008

UoC Quickie - Rapid Login Bookmarklet

What?

A one click solution to log in to the University of Chicago wireless network - the UoC Quickie3. Drag the link to your bookmark toolbar. Next time you want to log in to the UoC wireless, just click the bookmark. It'll take care of the rest.


Why?
  1. It's annoying to type in your cnet username and password every time you want to check your email.
  2. It's very annoying when on your handheld (iPhone...) to type in your cnet username and password every time you want to check your email
How?

Bookmarks are url's, and url's can contain javascript. This particular javascript
  1. checks that we're on the UoC login page
  2. check if it has stored login information
  3. uses the stored login information to log in, or asks for user name and password
The user name and password are stored as encrypted cookies. However, as always: guard your cookies.

To use the UoC Quickie3, just drag the link to your bookmark toolbar. Next next time you're faced with the UoC wireless login page, just click the bookmark.

Update: If you're an IE7 user, then you will have to right click the link and "add to favorites." If you're an IE6 user... you're out of luck. Microsoft saw to making this impossible. Please download Firefox. IE6 breaks the web.

Update: To get this to work on your iPhone, you have to add it to your Safari bookmarks on your computer and then sync. Unfortunately, there is no other way to get it onto your iPhone.

Update: A missing space in the cookie code messed up the expiration date, resulting in the cookies being trashed when the browser closed. This has now been fixed.

Update: New version! UoC Quickie3 now works on the iPhone. However, make sure you enter your information correctly - the only way to delete it is to manually delete your cookies.