One resource has been James Edward Gray II's book on TextMate published by the Pragmatic Programmers. I finally sat down and got serious about writing some automations of my own.
I've got to say that I'm pretty impressed by TextMate. At the last Raleigh.rb hack night I was talking to another vim user. I'd mentioned to him that you can extend TextMate easily in Ruby, without really having experienced it. Today, I wrote a neat little TextMate command to help in building Rails database test fixtures.
It acts like a tab triggered snippet, but it's a smart little critter. If I'm editing a fixture file, say 'test/fixtures/users.yml', I can type item then tab and it will produce the skeleton yaml for a new record, with:
- A dummy name selected for overtyping.
- The id set to the next available primary key Each column name as a yaml key ...
- ... A tabstop on a value which shows the column type
For example:
Suppose I've got a fixture file for a model called Item:
# == Schema Information # Schema version: 14 # # Table name: items # # id :integer(11) not null, primary key # description :string(255) # name :string(255) # price :integer(11) # # Read about fixtures at http://ar.rubyonrails.org/classes/Fixtures.html lotr1: id: 1 name: LOTR1 description: The Road Goes Ever On lotr2: id: 2 name: LOTR2 description: Introduction to Elvish
If I type a new line at the end of this file with the snippet command item and hit tab I see this:
lotr2:
id: 2
name: LOTR2
description: Introduction to Elvish
NameMe:
id: 3
description: string
name: string
price: integer
With NameMe selected so that I can easily replace it by typing. Note that the id was set to 3 since rows with ids 1 and 2 already exist.
Hitting tab again selects the first instance of string, and subsequent tabs step through the columns
Another feature which I won't show is that it looks at the table and if it doesn't have a primary key (e.g. for an association table in a has_and_belongs_to_many association), it won't generate the id: or a key.
You might be impressed, but I'm pretty happy with the results of a couple of hours of playing with TextMata and Ruby.
How it works.
The whole thing is done with a TextMate command implemented in Ruby. A few notes. First, it's just a coincidence that the snippet trigger is item and my example table is called items. Second, I'm not using the comments at the top of the fixture file which were generated when the model was created. The command works off of db/schema.db.
Here's the code:
#!/usr/bin/env ruby -w # # Created by Rick DeNatale on 2007-10-09. # Copyright (c) 2007. You may use this under the ruby license. $LOAD_PATH << "#{ENV['TM_SUPPORT_PATH']}/lib" require 'exit_codes.rb' class FixtureSnipGen attr_reader :rails_root, :table_name def initialize(fixture_file) match = %r{(.*)test/fixtures/(.*).yml}.match(ENV["TM_FILEPATH"]) @rails_root = match[1] @table_name = match[2] @no_id = false end def next_id max_id = 0 while line = gets claimed = %r{\s+id:\s+(\d+)}.match(line) max_id = [max_id, claimed[1].to_i].max if claimed end max_id + 1 end def gen_snippet cols = gen_columns if cols puts "${1:NameMe}:" puts " id: #{next_id}" unless @no_id puts cols.join("\n") else puts "Couldn't find #{table_name} in schema.rb" TextMate.exit_show_tool_tip end puts "$0" end def schema_file_name "#{rails_root}db/schema.rb" end def value_tab(text) @tab_stop += 1 (@tab_stop < 10) ? "${#{@tab_stop}:#{text}}" : " # #{text}" end def gen_columns # raise Exception.new("No schema file") unless File.exist?(schema_file_name) started = false @tab_stop = @no_id ? 0 : 1 result = [] File.new(schema_file_name).each do | line | if started break if %r{^(\s*)create_table\s+}.match(line) col_match = %r{\.column\s+\"(.*)\"\s*,\s*:(\w+)}.match(line) result << " #{col_match[1]}: #{value_tab(col_match[2])}" if col_match else started = %r{^(\s*)create_table\s+["']#{table_name}['"]}.match(line) @no_id = %r{:id\s+=>\s+false}.match(line) if started end end return result.empty? ? nil : result end end begin FixtureSnipGen.new(ENV["TM_FILEPATH"]).gen_snippet rescue Exception => ex puts ex TextMate.exit_show_tool_tip end
In the TextMate bundle editor, I just added a new command with this code, and set the following options:
- Save:
- Nothing
- Input:
- Entire Document - which pipes the entire textmate buffer for the file into stdin.
- Output:
- Insert as Snippet - which triggers TextMate to interpret the output as a snippet once the script has finished.
- Activation:
- Tab Trigger = item
- Scope Selector source.yaml - this only works within yml files.>
Finally note that although my example uses a table called items, it's just a coincidence that the tab trigger is item. It would be item in any fixture file.
The other day I wrote about problems with undeclared fixtures in Rails ActiveRecord tests.
Here’s a little code snippet which might be useful in tracking down such problems.
class MyTest < Test::Unit::TestCase
def self.been_here
result = @been_here
@been_here = true
result
end
class RowCounter < ActiveRecord::Base
end
def setup
unless self.class.been_here
File.open("#{RAILS_ROOT}/data_dump_#{Time.now.strftime("%m%d%H%M%S")}", "w") do |file|
Account.connection.tables.each do |table_name|
RowCounter.table_name = table_name
file.puts "#{table_name}: #{RowCounter.count}"
end
end
end
# The rest of your setup code goes here
end
# And the rest of the TestCase code here
endThis will dump a list of each table with a count of its rows the first time setup is run for the testcase. The file name is generated with a time stamp
Okay, so the example does go along with this weeks Harry Potter fever.
rails_developer@hogwarts.edu.magic.uk/cauldron_project/trunk$ cat data_dump_0717105533 students: 17 professors: 2 owls: 2 potions: 2 magical_creatures: 2 muggles: 0
Now you can run the test individually and then along with others. Then use a diff tool to see which tables have different numbers of rows before your test case starts for the first time.
A test helper
If you find this useful, why not add it to a test helper. If you don’t already have one, edit your test/test_helper.rb file, and add the following code inside of the Test::Unit::Testcase class:
def self.been_here
result = @been_here
@been_here = true
result
end
class RowCounter < ActiveRecord::Base
end
def dump_table_counts(file_name)
File.open(file_name, "w") do |file|
Account.connection.tables.each do |table_name|
RowCounter.table_name = table_name
file.puts "#{table_name}: #{RowCounter.count}"
end
end
endI’ve moved the definition of the been_here method and the class Rowcounter from the individual testcase to the superclass. The only code in the testcase needed to use this is in the setup method, which now becomes:
def setup
dump_table_counts("#{RAILS_ROOT}/data_dump_#{Time.now.strftime("%m%d%H%M%S")}") unless self.class.been_here
# rest of setup code here
endThe other day I was working on adding support for user selected time-zones to an existing rails app for a client. As usual I was doing test-first development. One of the things that makes rails such a pleasure is that a good set of tests give confidence that you aren’t breaking “legacy” code.
I also use, and really like, the rails plugin for vim which does lots of nice things like making navigation between the files of rails apps much easier. It also has a nice feature which adds a :Rake command to vim which “does the right” thing contextually. For example if you are editing a migration and enter :Rake, it runs rake db:migrate. If you are in a test file it runs just the single test selected by the cursor, or just that test file if you aren’t positioned to a particular test.
I was doing the latter, and my test was failing, and I was having a hard time debugging it. I tried executing just that single test from the bash command line with:
$ruby test/unit/my_test.rb -n"test_mytest"
and it still failed, no surprise. The same thing happened if I ran the entire test file, in fact, other tests which had worked before were now failing, and I was really mystified now because I didn’t see how I had done anything which had a remote chance of breaking those.
So I figured I probably should test everthing so:
$rake test
And, surprise of surprises, but every test worked. Not only my failing unit tests, but the functional and integration tests as well.
To make a long story short, the problem turned out to be that the tests were failing because, now that the particular model was sensitive to the user’s timezone, it needed access to it’s associated user model, and I hadn’t told the testcase that the users fixture was needed. Running the testcase in isolation, the users table wasn’t being populated for the test case. But running rake test, or rake test:units meant that other tests run before had left the data I needed behind.
Just a little thing that makes fixtures less than ideal.




