aboutsummaryrefslogblamecommitdiffstats
path: root/activerecord/lib/active_record/fixtures.rb
blob: 7a00b5bb568eddaafae2896f1cf11deb67ebee62 (plain) (tree)
1
2
3
4
5
6
7
8
9

              
             
 




                                    

   





                                                                       

   
                                                                                                                     
 


                            
 
                 
 
                                                                                                                         
                                                                     
 
                                                                                                                               


                                                                                                                        
 



                                     
 




                                
                                                                                                                     
                                                                                                                      
           
 













                                                                                                                                  

                
                                                                                                               
                                                                                                                                
 
                                                                                                                      










                                                                                                                           
                                                                                                                     


                                                                                                                  
                                                                                                                     
                                       

                        
 
                                                                                                                                     
                                                                                                                             





















                                                                                                                                       
                                                              





                                            
         
       

                                                                                                                        





                                                                          
 
                                                                                                                             
                                                                                                            
                                                                                          








                                                                                                                                 
                 
                                                                       

       

                                                                                                                    
                                                                                                                              
                                                                       
 





                                                                                                          
                                                      
 









                                                                            
                             
 

                                                                                                                           
 




                        
 
                                                  

                                                                                                                            
                                                                                                                            

                                                                                                                             


                          
                                                                                                                                   




                                                                                                         
    
                    
    
                       
                                     
                       
                                    
         
    
                                 
                                     

         
    
                                                                                                                         

                                                                                                                                          
                                                                                                                      

                                                                                                                                      


                                                                                                                                    
                                                                                                                    
                                                                                                                           
                                         












































                                                                          
                                                                          







































                                                                             
























                                                                                       










































































































                                                                              
                           

                                
                            


                                                

                                                    
 



                                                      
 













                                                                           
                                                                            
     

                                                                                    
                                                                              
                     
                                   
                                        



                                                                 
             
           
         

       
 
                                                                         

                                                                                 
       
     
 
                                     
                               
 
                                                                             
                                                          

                                                                      

                                                                                                         

                                      

                                                   
 


                                                                                                                                                                                 
 
                                                  
 


                                                                                  
 




                                                          
               
             
 

                                              
         
       
                                            

     






                                                                  

                         
                                                                                                   
                                                                                                             
                               
                                                                                                                      
                                                                                                           

                                                                              
                      


                              
                                                                                                  


                     













                                                                                     





                                                                                                      






                                               
                                             


                                                                     
 








                                                                              

                                



                                                                                                     









                                                                                                            








                                                                                 
                                                                          






                                                                                            
                                                      
       





                                                

     
         












                                                                  


                                                                    
       
 





                                                                                                  



                                                                                



                                                                        
                          
                                   
                               
                                     
                              
          
                           




                                                       



           

























                                                                                                   

       












                                                                                             
       



                            
 



                                                   





                                                                                                                                                                                                                                                                                 






                                     

                                             
     

                                           
     
 

                         
                                     





                                             
                                                                                                               

       















                                       

                                                                                                                 


                


                                                          
                                                                                                                    
                                                                                                             

               
     
 
          

                                                                                             
                                         

                                                                                   
       
     
 



                                                                   
                               


                                                                          
                                                                                                                  



                                                 
                                                                                                 





                                  

                      
                           





                                                                                                 



                                             
                                      
 
                                    
                                   
 


                                                                              
 
                                     






                                                                                   



                                               
 

                                                                 

                                                                                       
               
                                        

                                                              

             
         
 
                                                         
                                                                 



                                                                                            
 
                                             
 







                                                                                                        
               

                                                             

             
         
 
                                         

                                                                 
         
 
                                        

                                                                 


                                     

                                                    

         
                             
                                                                                               
 
                                                             
                                                                                       

           
                           
 
                                                   
                                      





                                                                    
                                                              
                                                            

                                       
                              

                                                     




                                                           


                                               
                                                                                               
 

                                          
           
 
                                                      
                                                                                  
                                                               
                                                 
           
                                                     
         






                                                         
                                    






                                                              
                                       


                                       


             
 
             

                               
                                                                                                     


                                                              
                

                                                                      
             
           
 

                                                                                        
 
                                


                                                                                                                 
                                                                                  


                                                                           
              



                                                                                                   
             
           
 


                                                    
       
     
   
require 'erb'
require 'yaml'
require 'csv'

module YAML #:nodoc:
  class Omap #:nodoc:
    def keys;   map { |k, v| k } end
    def values; map { |k, v| v } end
  end
end

if defined? ActiveRecord
  class FixtureClassNotFound < ActiveRecord::ActiveRecordError #:nodoc:
  end
else
  class FixtureClassNotFound < StandardError #:nodoc:
  end
end

# Fixtures are a way of organizing data that you want to test against; in short, sample data. They come in 3 flavors:
#
#   1.  YAML fixtures
#   2.  CSV fixtures
#   3.  Single-file fixtures
#
# = YAML fixtures
#
# This type of fixture is in YAML format and the preferred default. YAML is a file format which describes data structures
# in a non-verbose, human-readable format. It ships with Ruby 1.8.1+.
#
# Unlike single-file fixtures, YAML fixtures are stored in a single file per model, which are placed in the directory appointed
# by <tt>Test::Unit::TestCase.fixture_path=(path)</tt> (this is automatically configured for Rails, so you can just
# put your files in <your-rails-app>/test/fixtures/). The fixture file ends with the .yml file extension (Rails example:
# "<your-rails-app>/test/fixtures/web_sites.yml"). The format of a YAML fixture file looks like this:
#
#   rubyonrails:
#     id: 1
#     name: Ruby on Rails
#     url: http://www.rubyonrails.org
#
#   google:
#     id: 2
#     name: Google
#     url: http://www.google.com
#
# This YAML fixture file includes two fixtures.  Each YAML fixture (ie. record) is given a name and is followed by an
# indented list of key/value pairs in the "key: value" format.  Records are separated by a blank line for your viewing
# pleasure.
#
# Note that YAML fixtures are unordered. If you want ordered fixtures, use the omap YAML type.  See http://yaml.org/type/omap.html
# for the specification.  You will need ordered fixtures when you have foreign key constraints on keys in the same table.
# This is commonly needed for tree structures.  Example:
#
#    --- !omap
#    - parent:
#        id:         1
#        parent_id:  NULL
#        title:      Parent
#    - child:
#        id:         2
#        parent_id:  1
#        title:      Child
#
# = CSV fixtures
#
# Fixtures can also be kept in the Comma Separated Value format. Akin to YAML fixtures, CSV fixtures are stored
# in a single file, but instead end with the .csv file extension (Rails example: "<your-rails-app>/test/fixtures/web_sites.csv")
#
# The format of this type of fixture file is much more compact than the others, but also a little harder to read by us
# humans.  The first line of the CSV file is a comma-separated list of field names.  The rest of the file is then comprised
# of the actual data (1 per line).  Here's an example:
#
#   id, name, url
#   1, Ruby On Rails, http://www.rubyonrails.org
#   2, Google, http://www.google.com
#
# Should you have a piece of data with a comma character in it, you can place double quotes around that value.  If you
# need to use a double quote character, you must escape it with another double quote.
#
# Another unique attribute of the CSV fixture is that it has *no* fixture name like the other two formats.  Instead, the
# fixture names are automatically generated by deriving the class name of the fixture file and adding an incrementing
# number to the end.  In our example, the 1st fixture would be called "web_site_1" and the 2nd one would be called
# "web_site_2".
#
# Most databases and spreadsheets support exporting to CSV format, so this is a great format for you to choose if you
# have existing data somewhere already.
#
# = Single-file fixtures
#
# This type of fixture was the original format for Active Record that has since been deprecated in favor of the YAML and CSV formats.
# Fixtures for this format are created by placing text files in a sub-directory (with the name of the model) to the directory
# appointed by <tt>Test::Unit::TestCase.fixture_path=(path)</tt> (this is automatically configured for Rails, so you can just
# put your files in <your-rails-app>/test/fixtures/<your-model-name>/ -- like <your-rails-app>/test/fixtures/web_sites/ for the WebSite
# model).
#
# Each text file placed in this directory represents a "record".  Usually these types of fixtures are named without
# extensions, but if you are on a Windows machine, you might consider adding .txt as the extension.  Here's what the
# above example might look like:
#
#   web_sites/google
#   web_sites/yahoo.txt
#   web_sites/ruby-on-rails
#
# The file format of a standard fixture is simple.  Each line is a property (or column in db speak) and has the syntax
# of "name => value".  Here's an example of the ruby-on-rails fixture above:
#
#   id => 1
#   name => Ruby on Rails
#   url => http://www.rubyonrails.org
#
# = Using Fixtures
#
# Since fixtures are a testing construct, we use them in our unit and functional tests.  There are two ways to use the
# fixtures, but first let's take a look at a sample unit test:
#
#   require 'web_site'
#
#   class WebSiteTest < Test::Unit::TestCase
#     def test_web_site_count
#       assert_equal 2, WebSite.count
#     end
#   end
#
# As it stands, unless we pre-load the web_site table in our database with two records, this test will fail.  Here's the
# easiest way to add fixtures to the database:
#
#   ...
#   class WebSiteTest < Test::Unit::TestCase
#     fixtures :web_sites # add more by separating the symbols with commas
#   ...
#
# By adding a "fixtures" method to the test case and passing it a list of symbols (only one is shown here though), we trigger
# the testing environment to automatically load the appropriate fixtures into the database before each test.
# To ensure consistent data, the environment deletes the fixtures before running the load.
#
# In addition to being available in the database, the fixtures are also loaded into a hash stored in an instance variable
# of the test case.  It is named after the symbol... so, in our example, there would be a hash available called
# @web_sites.  This is where the "fixture name" comes into play.
#
# On top of that, each record is automatically "found" (using Model.find(id)) and placed in the instance variable of its name.
# So for the YAML fixtures, we'd get @rubyonrails and @google, which could be interrogated using regular Active Record semantics:
#
#   # test if the object created from the fixture data has the same attributes as the data itself
#   def test_find
#     assert_equal @web_sites["rubyonrails"]["name"], @rubyonrails.name
#   end
#
# As seen above, the data hash created from the YAML fixtures would have @web_sites["rubyonrails"]["url"] return
# "http://www.rubyonrails.org" and @web_sites["google"]["name"] would return "Google". The same fixtures, but loaded
# from a CSV fixture file, would be accessible via @web_sites["web_site_1"]["name"] == "Ruby on Rails" and have the individual
# fixtures available as instance variables @web_site_1 and @web_site_2.
#
# If you do not wish to use instantiated fixtures (usually for performance reasons) there are two options.
#
#   - to completely disable instantiated fixtures:
#       self.use_instantiated_fixtures = false
#
#   - to keep the fixture instance (@web_sites) available, but do not automatically 'find' each instance:
#       self.use_instantiated_fixtures = :no_instances
#
# Even if auto-instantiated fixtures are disabled, you can still access them
# by name via special dynamic methods. Each method has the same name as the
# model, and accepts the name of the fixture to instantiate:
#
#   fixtures :web_sites
#
#   def test_find
#     assert_equal "Ruby on Rails", web_sites(:rubyonrails).name
#   end
#
# = Dynamic fixtures with ERb
#
# Some times you don't care about the content of the fixtures as much as you care about the volume. In these cases, you can
# mix ERb in with your YAML or CSV fixtures to create a bunch of fixtures for load testing, like:
#
# <% for i in 1..1000 %>
# fix_<%= i %>:
#   id: <%= i %>
#   name: guy_<%= 1 %>
# <% end %>
#
# This will create 1000 very simple YAML fixtures.
#
# Using ERb, you can also inject dynamic values into your fixtures with inserts like <%= Date.today.strftime("%Y-%m-%d") %>.
# This is however a feature to be used with some caution. The point of fixtures are that they're stable units of predictable
# sample data. If you feel that you need to inject dynamic values, then perhaps you should reexamine whether your application
# is properly testable. Hence, dynamic values in fixtures are to be considered a code smell.
#
# = Transactional fixtures
#
# TestCases can use begin+rollback to isolate their changes to the database instead of having to delete+insert for every test case.
# They can also turn off auto-instantiation of fixture data since the feature is costly and often unused.
#
#   class FooTest < Test::Unit::TestCase
#     self.use_transactional_fixtures = true
#     self.use_instantiated_fixtures = false
#   
#     fixtures :foos
#   
#     def test_godzilla
#       assert !Foo.find(:all).empty?
#       Foo.destroy_all
#       assert Foo.find(:all).empty?
#     end
#   
#     def test_godzilla_aftermath
#       assert !Foo.find(:all).empty?
#     end
#   end
#   
# If you preload your test database with all fixture data (probably in the Rakefile task) and use transactional fixtures,
# then you may omit all fixtures declarations in your test cases since all the data's already there and every case rolls back its changes.
#
# In order to use instantiated fixtures with preloaded data, set +self.pre_loaded_fixtures+ to true. This will provide
# access to fixture data for every table that has been loaded through fixtures (depending on the value of +use_instantiated_fixtures+)
#
# When *not* to use transactional fixtures:
#   1. You're testing whether a transaction works correctly. Nested transactions don't commit until all parent transactions commit,
#      particularly, the fixtures transaction which is begun in setup and rolled back in teardown. Thus, you won't be able to verify
#      the results of your transaction until Active Record supports nested transactions or savepoints (in progress).
#   2. Your database does not support transactions. Every Active Record database supports transactions except MySQL MyISAM.
#      Use InnoDB, MaxDB, or NDB instead.
#
# = Advanced YAML Fixtures
#
# YAML fixtures that don't specify an ID get some extra features:
#
# * Stable, autogenerated ID's
# * Label references for associations (belongs_to, has_one, has_many)
# * HABTM associations as inline lists
# * Autofilled timestamp columns
# * Fixture label interpolation
# * Support for YAML defaults
#
# == Stable, autogenerated ID's
#
# Here, have a monkey fixture:
#
#   george:
#     id: 1
#     name: George the Monkey
#
#   reginald:
#     id: 2
#     name: Reginald the Pirate
#
# Each of these fixtures has two unique identifiers: one for the database
# and one for the humans. Why don't we generate the primary key instead?
# Hashing each fixture's label yields a consistent ID:
#
#   george: # generated id: 503576764
#     name: George the Monkey
#
#   reginald: # generated id: 324201669
#     name: Reginald the Pirate
#
# ActiveRecord looks at the fixture's model class, discovers the correct
# primary key, and generates it right before inserting the fixture
# into the database.
#
# The generated ID for a given label is constant, so we can discover
# any fixture's ID without loading anything, as long as we know the label.
#
# == Label references for associations (belongs_to, has_one, has_many)
#
# Specifying foreign keys in fixtures can be very fragile, not to
# mention difficult to read. Since ActiveRecord can figure out the ID of
# any fixture from its label, you can specify FK's by label instead of ID.
#
# === belongs_to
#
# Let's break out some more monkeys and pirates.
#
#   ### in pirates.yml
#
#   reginald:
#     id: 1
#     name: Reginald the Pirate
#     monkey_id: 1
#
#   ### in monkeys.yml
#
#   george:
#     id: 1
#     name: George the Monkey
#     pirate_id: 1
#
# Add a few more monkeys and pirates and break this into multiple files,
# and it gets pretty hard to keep track of what's going on. Let's
# use labels instead of ID's:
#
#   ### in pirates.yml
#
#   reginald:
#     name: Reginald the Pirate
#     monkey: george
#
#   ### in monkeys.yml
#
#   george:
#     name: George the Monkey
#     pirate: reginald
#
# Pow! All is made clear. ActiveRecord reflects on the fixture's model class,
# finds all the +belongs_to+ associations, and allows you to specify
# a target *label* for the *association* (monkey: george) rather than
# a target *id* for the *FK* (monkey_id: 1).
#
# ==== Polymorphic belongs_to
#
# Supporting polymorphic relationships is a little bit more complicated, since
# ActiveRecord needs to know what type your association is pointing at. Something
# like this should look familiar:
#
#   ### in fruit.rb
#
#   belongs_to :eater, :polymorphic => true
#
#   ### in fruits.yml
#
#   apple:
#     id: 1
#     name: apple
#     eater_id: 1
#     eater_type: Monkey
#
# Can we do better? You bet!
#
#   apple:
#     eater: george (Monkey)
#
# Just provide the polymorphic target type and ActiveRecord will take care of the rest.
#
# === has_and_belongs_to_many
#
# Time to give our monkey some fruit.
#
#   ### in monkeys.yml
#
#   george:
#     id: 1
#     name: George the Monkey
#     pirate_id: 1
#
#   ### in fruits.yml
#
#   apple:
#     id: 1
#     name: apple
#
#   orange:
#     id: 2
#     name: orange
#
#   grape:
#     id: 3
#     name: grape
#
#   ### in fruits_monkeys.yml
#
#   apple_george:
#     fruit_id: 1
#     monkey_id: 1
#
#   orange_george:
#     fruit_id: 2
#     monkey_id: 1
#
#   grape_george:
#     fruit_id: 3
#     monkey_id: 1
#
# Let's make the HABTM fixture go away.
#
#   ### in monkeys.yml
#
#   george:
#     name: George the Monkey
#     pirate: reginald
#     fruits: apple, orange, grape
#
#   ### in fruits.yml
#
#   apple:
#     name: apple
#
#   orange:
#     name: orange
#
#   grape:
#     name: grape
#
# Zap! No more fruits_monkeys.yml file. We've specified the list of fruits
# on George's fixture, but we could've just as easily specified a list
# of monkeys on each fruit. As with +belongs_to+, ActiveRecord reflects on
# the fixture's model class and discovers the +has_and_belongs_to_many+
# associations.
#
# == Autofilled timestamp columns
#
# If your table/model specifies any of ActiveRecord's
# standard timestamp columns (created_at, created_on, updated_at, updated_on),
# they will automatically be set to Time.now.
#
# If you've set specific values, they'll be left alone.
#
# == Fixture label interpolation
#
# The label of the current fixture is always available as a column value:
#
#   geeksomnia:
#     name: Geeksomnia's Account
#     subdomain: $LABEL
#
# Also, sometimes (like when porting older join table fixtures) you'll need
# to be able to get ahold of the identifier for a given label. ERB
# to the rescue:
#
#   george_reginald:
#     monkey_id: <%= Fixtures.identify(:reginald) %>
#     pirate_id: <%= Fixtures.identify(:george) %>
#
# == Support for YAML defaults
#
# You probably already know how to use YAML to set and reuse defaults in
# your +database.yml+ file,. You can use the same technique in your fixtures:
#
#   DEFAULTS: &DEFAULTS
#     created_on: <%= 3.weeks.ago.to_s(:db) %>
#
#   first:
#     name: Smurf
#     <<: *DEFAULTS
#
#   second:
#     name: Fraggle
#     <<: *DEFAULTS
#
# Any fixture labeled "DEFAULTS" is safely ignored.

class Fixtures < YAML::Omap
  DEFAULT_FILTER_RE = /\.ya?ml$/

  @@all_cached_fixtures = {}

  def self.reset_cache(connection = nil)
    connection ||= ActiveRecord::Base.connection
    @@all_cached_fixtures[connection.object_id] = {}
  end

  def self.cache_for_connection(connection)
    @@all_cached_fixtures[connection.object_id] ||= {}
    @@all_cached_fixtures[connection.object_id]
  end

  def self.fixture_is_cached?(connection, table_name)
    cache_for_connection(connection)[table_name]
  end

  def self.cached_fixtures(connection, keys_to_fetch = nil)
    if keys_to_fetch
      fixtures = cache_for_connection(connection).values_at(*keys_to_fetch)
    else
      fixtures = cache_for_connection(connection).values
    end
    fixtures.size > 1 ? fixtures : fixtures.first
  end

  def self.cache_fixtures(connection, fixtures)
    cache_for_connection(connection).update(fixtures.index_by(&:table_name))
  end

  def self.instantiate_fixtures(object, table_name, fixtures, load_instances = true)
    object.instance_variable_set "@#{table_name.to_s.gsub('.','_')}", fixtures
    if load_instances
      ActiveRecord::Base.silence do
        fixtures.each do |name, fixture|
          begin
            object.instance_variable_set "@#{name}", fixture.find
          rescue FixtureClassNotFound
            nil
          end
        end
      end
    end
  end

  def self.instantiate_all_loaded_fixtures(object, load_instances = true)
    all_loaded_fixtures.each do |table_name, fixtures|
      Fixtures.instantiate_fixtures(object, table_name, fixtures, load_instances)
    end
  end

  cattr_accessor :all_loaded_fixtures
  self.all_loaded_fixtures = {}

  def self.create_fixtures(fixtures_directory, table_names, class_names = {})
    table_names = [table_names].flatten.map { |n| n.to_s }
    connection  = block_given? ? yield : ActiveRecord::Base.connection

    table_names_to_fetch = table_names.reject { |table_name| fixture_is_cached?(connection, table_name) }

    unless table_names_to_fetch.empty?
      ActiveRecord::Base.silence do
        connection.disable_referential_integrity do
          fixtures_map = {}

          fixtures = table_names_to_fetch.map do |table_name|
            fixtures_map[table_name] = Fixtures.new(connection, File.split(table_name.to_s).last, class_names[table_name.to_sym], File.join(fixtures_directory, table_name.to_s))
          end

          all_loaded_fixtures.update(fixtures_map)

          connection.transaction(Thread.current['open_transactions'].to_i == 0) do
            fixtures.reverse.each { |fixture| fixture.delete_existing_fixtures }
            fixtures.each { |fixture| fixture.insert_fixtures }

            # Cap primary key sequences to max(pk).
            if connection.respond_to?(:reset_pk_sequence!)
              table_names.each do |table_name|
                connection.reset_pk_sequence!(table_name)
              end
            end
          end

          cache_fixtures(connection, fixtures)
        end
      end
    end
    cached_fixtures(connection, table_names)
  end

  # Returns a consistent identifier for +label+. This will always
  # be a positive integer, and will always be the same for a given
  # label, assuming the same OS, platform, and version of Ruby.
  def self.identify(label)
    label.to_s.hash.abs
  end

  attr_reader :table_name

  def initialize(connection, table_name, class_name, fixture_path, file_filter = DEFAULT_FILTER_RE)
    @connection, @table_name, @fixture_path, @file_filter = connection, table_name, fixture_path, file_filter
    @class_name = class_name ||
                  (ActiveRecord::Base.pluralize_table_names ? @table_name.singularize.camelize : @table_name.camelize)
    @table_name = ActiveRecord::Base.table_name_prefix + @table_name + ActiveRecord::Base.table_name_suffix
    @table_name = class_name.table_name if class_name.respond_to?(:table_name)
    @connection = class_name.connection if class_name.respond_to?(:connection)
    read_fixture_files
  end

  def delete_existing_fixtures
    @connection.delete "DELETE FROM #{@connection.quote_table_name(table_name)}", 'Fixture Delete'
  end

  def insert_fixtures
    now = ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now
    now = now.to_s(:db)

    # allow a standard key to be used for doing defaults in YAML
    delete(assoc("DEFAULTS"))

    # track any join tables we need to insert later
    habtm_fixtures = Hash.new do |h, habtm|
      h[habtm] = HabtmFixtures.new(@connection, habtm.options[:join_table], nil, nil)
    end

    each do |label, fixture|
      row = fixture.to_hash

      if model_class && model_class < ActiveRecord::Base
        # fill in timestamp columns if they aren't specified and the model is set to record_timestamps
        if model_class.record_timestamps
          timestamp_column_names.each do |name|
            row[name] = now unless row.key?(name)
          end
        end

        # interpolate the fixture label
        row.each do |key, value|
          row[key] = label if value == "$LABEL"
        end

        # generate a primary key if necessary
        if has_primary_key_column? && !row.include?(primary_key_name)
          row[primary_key_name] = Fixtures.identify(label)
        end

        # If STI is used, find the correct subclass for association reflection
        reflection_class =
          if row.include?(inheritance_column_name)
            row[inheritance_column_name].constantize rescue model_class
          else
            model_class
          end

        reflection_class.reflect_on_all_associations.each do |association|
          case association.macro
          when :belongs_to
            # Do not replace association name with association foreign key if they are named the same
            fk_name = (association.options[:foreign_key] || "#{association.name}_id").to_s

            if association.name.to_s != fk_name && value = row.delete(association.name.to_s)
              if association.options[:polymorphic]
                if value.sub!(/\s*\(([^\)]*)\)\s*$/, "")
                  target_type = $1
                  target_type_name = (association.options[:foreign_type] || "#{association.name}_type").to_s

                  # support polymorphic belongs_to as "label (Type)"
                  row[target_type_name] = target_type
                end
              end

              row[fk_name] = Fixtures.identify(value)
            end
          when :has_and_belongs_to_many
            if (targets = row.delete(association.name.to_s))
              targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/)
              join_fixtures = habtm_fixtures[association]

              targets.each do |target|
                join_fixtures["#{label}_#{target}"] = Fixture.new(
                  { association.primary_key_name => row[primary_key_name],
                    association.association_foreign_key => Fixtures.identify(target) }, nil)
              end
            end
          end
        end
      end

      @connection.insert_fixture(fixture, @table_name)
    end

    # insert any HABTM join tables we discovered
    habtm_fixtures.values.each do |fixture|
      fixture.delete_existing_fixtures
      fixture.insert_fixtures
    end
  end

  private
    class HabtmFixtures < ::Fixtures #:nodoc:
      def read_fixture_files; end
    end

    def model_class
      @model_class ||= @class_name.is_a?(Class) ?
        @class_name : @class_name.constantize rescue nil
    end

    def primary_key_name
      @primary_key_name ||= model_class && model_class.primary_key
    end

    def has_primary_key_column?
      @has_primary_key_column ||= model_class && primary_key_name &&
        model_class.columns.find { |c| c.name == primary_key_name }
    end

    def timestamp_column_names
      @timestamp_column_names ||= %w(created_at created_on updated_at updated_on).select do |name|
        column_names.include?(name)
      end
    end

    def inheritance_column_name
      @inheritance_column_name ||= model_class && model_class.inheritance_column
    end

    def column_names
      @column_names ||= @connection.columns(@table_name).collect(&:name)
    end

    def read_fixture_files
      if File.file?(yaml_file_path)
        read_yaml_fixture_files
      elsif File.file?(csv_file_path)
        read_csv_fixture_files
      else
        # Standard fixtures
        Dir.entries(@fixture_path).each do |file|
          path = File.join(@fixture_path, file)
          if File.file?(path) and file !~ @file_filter
            self[file] = Fixture.new(path, @class_name)
          end
        end
      end
    end

    def read_yaml_fixture_files
      yaml_string = ""
      Dir["#{@fixture_path}/**/*.yml"].select { |f| test(?f, f) }.each do |subfixture_path|
        yaml_string << IO.read(subfixture_path)
      end
      yaml_string << IO.read(yaml_file_path)

      if yaml = parse_yaml_string(yaml_string)
        # If the file is an ordered map, extract its children.
        yaml_value =
          if yaml.respond_to?(:type_id) && yaml.respond_to?(:value)
            yaml.value
          else
            [yaml]
          end

        yaml_value.each do |fixture|
          fixture.each do |name, data|
            unless data
              raise Fixture::FormatError, "Bad data for #{@class_name} fixture named #{name} (nil)"
            end

            self[name] = Fixture.new(data, @class_name)
          end
        end
      end
    end

    def read_csv_fixture_files
      reader = CSV::Reader.create(erb_render(IO.read(csv_file_path)))
      header = reader.shift
      i = 0
      reader.each do |row|
        data = {}
        row.each_with_index { |cell, j| data[header[j].to_s.strip] = cell.to_s.strip }
        self["#{Inflector::underscore(@class_name)}_#{i+=1}"]= Fixture.new(data, @class_name)
      end
    end

    def yaml_file_path
      "#{@fixture_path}.yml"
    end

    def csv_file_path
      @fixture_path + ".csv"
    end

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

    def parse_yaml_string(fixture_content)
      YAML::load(erb_render(fixture_content))
    rescue => error
      raise Fixture::FormatError, "a YAML error occurred parsing #{yaml_file_path}. Please note that YAML must be consistently indented using spaces. Tabs are not allowed. Please have a look at http://www.yaml.org/faq.html\nThe exact error was:\n  #{error.class}: #{error}"
    end

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

class Fixture #:nodoc:
  include Enumerable

  class FixtureError < StandardError #:nodoc:
  end

  class FormatError < FixtureError #:nodoc:
  end

  attr_reader :class_name

  def initialize(fixture, class_name)
    case fixture
      when Hash, YAML::Omap
        @fixture = fixture
      when String
        @fixture = read_fixture_file(fixture)
      else
        raise ArgumentError, "Bad fixture argument #{fixture.inspect} during creation of #{class_name} fixture"
    end

    @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
    columns = @fixture.keys.collect{ |column_name| ActiveRecord::Base.connection.quote_column_name(column_name) }
    columns.join(", ")
  end

  def value_list
    klass = @class_name.constantize rescue nil

    list = @fixture.inject([]) do |fixtures, (key, value)|
      col = klass.columns_hash[key] if klass.respond_to?(:ancestors) && klass.ancestors.include?(ActiveRecord::Base)
      fixtures << ActiveRecord::Base.connection.quote(value, col).gsub('[^\]\\n', "\n").gsub('[^\]\\r', "\r")
    end
    list * ', '
  end

  def find
    klass = @class_name.is_a?(Class) ? @class_name : Object.const_get(@class_name) rescue nil
    if klass
      klass.find(self[klass.primary_key])
    else
      raise FixtureClassNotFound, "The class #{@class_name.inspect} was not found."
    end
  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 =~ /^\s*$/

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

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

module Test #:nodoc:
  module Unit #:nodoc:
    class TestCase #:nodoc:
      superclass_delegating_accessor :fixture_path
      superclass_delegating_accessor :fixture_table_names
      superclass_delegating_accessor :fixture_class_names
      superclass_delegating_accessor :use_transactional_fixtures
      superclass_delegating_accessor :use_instantiated_fixtures   # true, false, or :no_instances
      superclass_delegating_accessor :pre_loaded_fixtures

      self.fixture_table_names = []
      self.use_transactional_fixtures = false
      self.use_instantiated_fixtures = true
      self.pre_loaded_fixtures = false

      @@already_loaded_fixtures = {}
      self.fixture_class_names = {}

      def self.set_fixture_class(class_names = {})
        self.fixture_class_names = self.fixture_class_names.merge(class_names)
      end

      def self.fixtures(*table_names)
        if table_names.first == :all
          table_names = Dir["#{fixture_path}/*.yml"] + Dir["#{fixture_path}/*.csv"]
          table_names.map! { |f| File.basename(f).split('.')[0..-2].join('.') }
        else
          table_names = table_names.flatten.map { |n| n.to_s }
        end

        self.fixture_table_names |= table_names
        require_fixture_classes(table_names)
        setup_fixture_accessors(table_names)
      end

      def self.require_fixture_classes(table_names = nil)
        (table_names || fixture_table_names).each do |table_name|
          file_name = table_name.to_s
          file_name = file_name.singularize if ActiveRecord::Base.pluralize_table_names
          begin
            require_dependency file_name
          rescue LoadError
            # Let's hope the developer has included it himself
          end
        end
      end

      def self.setup_fixture_accessors(table_names = nil)
        (table_names || fixture_table_names).each do |table_name|
          table_name = table_name.to_s.tr('.', '_')

          define_method(table_name) do |*fixtures|
            force_reload = fixtures.pop if fixtures.last == true || fixtures.last == :reload

            @fixture_cache[table_name] ||= {}

            instances = fixtures.map do |fixture|
              @fixture_cache[table_name].delete(fixture) if force_reload

              if @loaded_fixtures[table_name][fixture.to_s]
                @fixture_cache[table_name][fixture] ||= @loaded_fixtures[table_name][fixture.to_s].find
              else
                raise StandardError, "No fixture with name '#{fixture}' found for table '#{table_name}'"
              end
            end

            instances.size == 1 ? instances.first : instances
          end
        end
      end

      def self.uses_transaction(*methods)
        @uses_transaction = [] unless defined?(@uses_transaction)
        @uses_transaction.concat methods.map(&:to_s)
      end

      def self.uses_transaction?(method)
        @uses_transaction = [] unless defined?(@uses_transaction)
        @uses_transaction.include?(method.to_s)
      end

      def use_transactional_fixtures?
        use_transactional_fixtures &&
          !self.class.uses_transaction?(method_name)
      end

      def setup_with_fixtures
        return unless defined?(ActiveRecord::Base) && !ActiveRecord::Base.configurations.blank?

        if pre_loaded_fixtures && !use_transactional_fixtures
          raise RuntimeError, 'pre_loaded_fixtures requires use_transactional_fixtures'
        end

        @fixture_cache = {}

        # Load fixtures once and begin transaction.
        if use_transactional_fixtures?
          if @@already_loaded_fixtures[self.class]
            @loaded_fixtures = @@already_loaded_fixtures[self.class]
          else
            load_fixtures
            @@already_loaded_fixtures[self.class] = @loaded_fixtures
          end
          ActiveRecord::Base.send :increment_open_transactions
          ActiveRecord::Base.connection.begin_db_transaction
        # Load fixtures for every test.
        else
          Fixtures.reset_cache
          @@already_loaded_fixtures[self.class] = nil
          load_fixtures
        end

        # Instantiate fixtures for every test if requested.
        instantiate_fixtures if use_instantiated_fixtures
      end
      alias_method :setup, :setup_with_fixtures

      def teardown_with_fixtures
        return unless defined?(ActiveRecord::Base) && !ActiveRecord::Base.configurations.blank?

        unless use_transactional_fixtures?
          Fixtures.reset_cache
        end

        # Rollback changes if a transaction is active.
        if use_transactional_fixtures? && Thread.current['open_transactions'] != 0
          ActiveRecord::Base.connection.rollback_db_transaction
          Thread.current['open_transactions'] = 0
        end
        ActiveRecord::Base.verify_active_connections!
      end
      alias_method :teardown, :teardown_with_fixtures

      def self.method_added(method)
        case method.to_s
        when 'setup'
          unless method_defined?(:setup_without_fixtures)
            alias_method :setup_without_fixtures, :setup
            define_method(:setup) do
              setup_with_fixtures
              setup_without_fixtures
            end
          end
        when 'teardown'
          unless method_defined?(:teardown_without_fixtures)
            alias_method :teardown_without_fixtures, :teardown
            define_method(:teardown) do
              teardown_without_fixtures
              teardown_with_fixtures
            end
          end
        end
      end

      private
        def load_fixtures
          @loaded_fixtures = {}
          fixtures = Fixtures.create_fixtures(fixture_path, fixture_table_names, fixture_class_names)
          unless fixtures.nil?
            if fixtures.instance_of?(Fixtures)
              @loaded_fixtures[fixtures.table_name] = fixtures
            else
              fixtures.each { |f| @loaded_fixtures[f.table_name] = f }
            end
          end
        end

        # for pre_loaded_fixtures, only require the classes once. huge speed improvement
        @@required_fixture_classes = false

        def instantiate_fixtures
          if pre_loaded_fixtures
            raise RuntimeError, 'Load fixtures before instantiating them.' if Fixtures.all_loaded_fixtures.empty?
            unless @@required_fixture_classes
              self.class.require_fixture_classes Fixtures.all_loaded_fixtures.keys
              @@required_fixture_classes = true
            end
            Fixtures.instantiate_all_loaded_fixtures(self, load_instances?)
          else
            raise RuntimeError, 'Load fixtures before instantiating them.' if @loaded_fixtures.nil?
            @loaded_fixtures.each do |table_name, fixtures|
              Fixtures.instantiate_fixtures(self, table_name, fixtures, load_instances?)
            end
          end
        end

        def load_instances?
          use_instantiated_fixtures != :no_instances
        end
    end
  end
end