aboutsummaryrefslogblamecommitdiffstats
path: root/activerecord/lib/active_record/fixtures.rb
blob: f17768e1f206aaa0a8312aa75893d6adb3cf69d9 (plain) (tree)















































































































































































































                                                                                                                                
require 'erb'
require 'yaml'
require 'active_record/support/class_inheritable_attributes'
require 'active_record/support/inflector'

# Fixtures are a way of organizing data that you want to test against. You normally have one YAML file with fixture
# definitions per model. They're just hashes of hashes with the first-level key being the name of fixture (try to keep
# that name unique across all fixtures in the system for reasons that will follow). The value to that key is a hash
# where the keys are column names and the values the fixture data you want to insert into it. Example for developers.yml:
#
# david:
#  id: 1
#  name: David Heinemeier Hansson
#  birthday: 1979-10-15
#  profession: Systems development
#
# steve:
#  id: 2
#  name: Steve Ross Kellock
#  birthday: 1974-09-27
#  profession: guy with keyboard
#
# So this YAML file includes two fixtures. T
#
# Now when we call <tt>@developers = Fixtures.create_fixtures(".", "developers")</tt> both developers will get inserted into 
# the "developers" table through the active Active Record connection (that must be setup before-hand). And we can now query 
# the fixture data through the <tt>@developers</tt> hash, so <tt>@developers["david"]["name"]</tt> will return 
# <tt>"David Heinemeier Hansson"</tt> and <tt>@developers["david"]["birthday"]</tt> will return <tt>Date.new(1979, 10, 15)</tt>.
#
# In addition to getting the raw data, we can also get the Developer object by doing @developers["david"].find. This can then 
# be used for comparison in a unit test. Something like:
#
#   def test_find
#     assert_equal @developers["david"]["name"], @developers["david"].find.name
#   end
#
# Comparing that the data we have on the name is also what the object returns when we ask for it.
#
# == Automatic fixture setup and instance variable availability
#
# Fixtures can also be automatically instantiated in instance variables relating to their names using the following style:
#
#   class FixturesTest < Test::Unit::TestCase
#     fixtures :developers # you can add more with comma separation
#
#     def test_developers
#       assert_equal 3, @developers.size # the container for all the fixtures is automatically set
#       assert_kind_of Developer, @david # works like @developers["david"].find
#       assert_equal "David Heinemeier Hansson", @david.name
#     end
#   end
class Fixtures < Hash
  def self.instantiate_fixtures(object, fixtures_directory, *table_names)
    [ create_fixtures(fixtures_directory, *table_names) ].flatten.each_with_index do |fixtures, idx|
      object.instance_variable_set "@#{table_names[idx]}", fixtures
      fixtures.each { |name, fixture| object.instance_variable_set "@#{name}", fixture.find }
    end
  end
  
  def self.create_fixtures(fixtures_directory, *table_names)
    connection = block_given? ? yield : ActiveRecord::Base.connection
    old_logger_level = ActiveRecord::Base.logger.level

    begin
      ActiveRecord::Base.logger.level = Logger::ERROR
      fixtures = connection.transaction do
        table_names.flatten.map do |table_name|
          Fixtures.new(connection, table_name.to_s, File.join(fixtures_directory, table_name.to_s))
        end
      end
      return fixtures.size > 1 ? fixtures : fixtures.first
    ensure
      ActiveRecord::Base.logger.level = old_logger_level
    end
  end

  def initialize(connection, table_name, fixture_path, file_filter = /^\.|CVS|\.yml/)
    @connection, @table_name, @fixture_path, @file_filter = connection, table_name, fixture_path, file_filter
    @class_name = Inflector.classify(@table_name)

    read_fixture_files
    delete_existing_fixtures
    insert_fixtures
  end

  private
    def read_fixture_files
      if File.exists?(yaml_file_path)
        YAML::load(erb_render(IO.read(yaml_file_path))).each do |name, data|
          self[name] = Fixture.new(data, @class_name)
        end
      else
        Dir.entries(@fixture_path).each do |file| 
          self[file] = Fixture.new(File.join(@fixture_path, file), @class_name) unless file =~ @file_filter
        end
      end
    end

    def delete_existing_fixtures
      @connection.delete "DELETE FROM #{@table_name}"
    end

    def insert_fixtures
      values.each do |fixture|
        @connection.execute "INSERT INTO #{@table_name} (#{fixture.key_list}) VALUES(#{fixture.value_list})"
      end
    end

    def yaml_file_path
      @fixture_path + ".yml"
    end
    
    def yaml_fixtures_key(path)
      File.basename(@fixture_path).split(".").first
    end

    def erb_render(fixture_content)
      ERB.new(fixture_content).result
    end
end

class Fixture #:nodoc:
  include Enumerable
  class FixtureError < StandardError; end
  class FormatError < FixtureError; end

  def initialize(fixture, class_name)
    @fixture = fixture.is_a?(Hash) ? fixture : read_fixture_file(fixture)
    @class_name = class_name
  end

  def each
    @fixture.each { |item| yield item }
  end

  def [](key)
    @fixture[key]
  end

  def to_hash
    @fixture
  end

  def key_list
    @fixture.keys.join(", ")
  end

  def value_list
    @fixture.values.map { |v| ActiveRecord::Base.connection.quote(v).gsub('\\n', "\n").gsub('\\r', "\r") }.join(", ")
  end
  
  def find
    Object.const_get(@class_name).find(self["id"])
  end
  
  private
    def read_fixture_file(fixture_file_path)
      IO.readlines(fixture_file_path).inject({}) do |fixture, line|
        # Mercifully skip empty lines.
        next if line.empty?

        # Use the same regular expression for attributes as Active Record.
        unless md = /^\s*([a-zA-Z][-_\w]*)\s*=>\s*(.+)\s*$/.match(line)
          raise FormatError, "#{path}: fixture format error at '#{line}'.  Expecting 'key => value'."
        end
        key, value = md.captures

        # Disallow duplicate keys to catch typos.
        raise FormatError, "#{path}: duplicate '#{key}' in fixture." if fixture[key]
        fixture[key] = value.strip
        fixture
      end
    end
end

class Test::Unit::TestCase #:nodoc:
  include ClassInheritableAttributes
  
  cattr_accessor :fixture_path
  cattr_accessor :fixture_table_names
  
  def self.fixtures(*table_names)
    write_inheritable_attribute("fixture_table_names", table_names)
  end

  def setup
    instantiate_fixtures(*fixture_table_names) if fixture_table_names
  end
  
  def self.method_added(method_symbol)
    if method_symbol == :setup && !method_defined?(:setup_without_fixtures)
      alias_method :setup_without_fixtures, :setup
      define_method(:setup) do
        instantiate_fixtures(*fixture_table_names) if fixture_table_names
        setup_without_fixtures
      end
    end
  end

  private
    def instantiate_fixtures(*table_names)
      Fixtures.instantiate_fixtures(self, fixture_path, *table_names)
    end
    
    def fixture_table_names
      self.class.read_inheritable_attribute("fixture_table_names")
    end
end