fixtures.rb 34.5 KB
Newer Older
D
Initial  
David Heinemeier Hansson 已提交
1 2
require 'erb'
require 'yaml'
3
require 'csv'
D
Initial  
David Heinemeier Hansson 已提交
4

5 6 7 8 9
module YAML #:nodoc:
  class Omap #:nodoc:
    def keys;   map { |k, v| k } end
    def values; map { |k, v| v } end
  end
10 11
end

12 13 14 15 16 17
if defined? ActiveRecord
  class FixtureClassNotFound < ActiveRecord::ActiveRecordError #:nodoc:
  end
else
  class FixtureClassNotFound < StandardError #:nodoc:
  end
18 19
end

20
# Fixtures are a way of organizing data that you want to test against; in short, sample data. They come in 3 flavors:
D
Initial  
David Heinemeier Hansson 已提交
21
#
22 23 24
#   1.  YAML fixtures
#   2.  CSV fixtures
#   3.  Single-file fixtures
D
Initial  
David Heinemeier Hansson 已提交
25
#
26
# = YAML fixtures
D
Initial  
David Heinemeier Hansson 已提交
27
#
28
# This type of fixture is in YAML format and the preferred default. YAML is a file format which describes data structures
29
# in a non-verbose, human-readable format. It ships with Ruby 1.8.1+.
D
Initial  
David Heinemeier Hansson 已提交
30
#
D
David Heinemeier Hansson 已提交
31
# Unlike single-file fixtures, YAML fixtures are stored in a single file per model, which are placed in the directory appointed
32 33 34
# 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:
D
Initial  
David Heinemeier Hansson 已提交
35
#
36 37 38 39
#   rubyonrails:
#     id: 1
#     name: Ruby on Rails
#     url: http://www.rubyonrails.org
D
Initial  
David Heinemeier Hansson 已提交
40
#
41 42 43 44 45
#   google:
#     id: 2
#     name: Google
#     url: http://www.google.com
#
46
# This YAML fixture file includes two fixtures.  Each YAML fixture (ie. record) is given a name and is followed by an
47
# indented list of key/value pairs in the "key: value" format.  Records are separated by a blank line for your viewing
48
# pleasure.
49
#
50 51 52 53 54 55 56 57 58 59 60 61 62 63
# 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
#
64 65
# = CSV fixtures
#
D
David Heinemeier Hansson 已提交
66
# Fixtures can also be kept in the Comma Separated Value format. Akin to YAML fixtures, CSV fixtures are stored
D
David Heinemeier Hansson 已提交
67
# in a single file, but instead end with the .csv file extension (Rails example: "<your-rails-app>/test/fixtures/web_sites.csv")
68
#
69
# The format of this type of fixture file is much more compact than the others, but also a little harder to read by us
70 71 72 73 74 75 76 77 78 79 80
# 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
81
# fixture names are automatically generated by deriving the class name of the fixture file and adding an incrementing
82 83 84
# 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".
#
85
# Most databases and spreadsheets support exporting to CSV format, so this is a great format for you to choose if you
86
# have existing data somewhere already.
87 88
#
# = Single-file fixtures
89
#
90
# This type of fixture was the original format for Active Record that has since been deprecated in favor of the YAML and CSV formats.
91
# Fixtures for this format are created by placing text files in a sub-directory (with the name of the model) to the directory
92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113
# 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
114
# fixtures, but first let's take a look at a sample unit test:
115 116 117 118 119 120
#
#   require 'web_site'
#
#   class WebSiteTest < Test::Unit::TestCase
#     def test_web_site_count
#       assert_equal 2, WebSite.count
121
#     end
122
#   end
123 124
#
# As it stands, unless we pre-load the web_site table in our database with two records, this test will fail.  Here's the
125 126 127 128 129 130
# easiest way to add fixtures to the database:
#
#   ...
#   class WebSiteTest < Test::Unit::TestCase
#     fixtures :web_sites # add more by separating the symbols with commas
#   ...
131
#
132
# By adding a "fixtures" method to the test case and passing it a list of symbols (only one is shown here though), we trigger
133
# the testing environment to automatically load the appropriate fixtures into the database before each test.
D
David Heinemeier Hansson 已提交
134
# To ensure consistent data, the environment deletes the fixtures before running the load.
135 136 137 138 139 140 141 142 143
#
# 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
D
Initial  
David Heinemeier Hansson 已提交
144
#   def test_find
145
#     assert_equal @web_sites["rubyonrails"]["name"], @rubyonrails.name
D
Initial  
David Heinemeier Hansson 已提交
146 147
#   end
#
148 149
# 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
D
David Heinemeier Hansson 已提交
150
# from a CSV fixture file, would be accessible via @web_sites["web_site_1"]["name"] == "Ruby on Rails" and have the individual
151
# fixtures available as instance variables @web_site_1 and @web_site_2.
D
Initial  
David Heinemeier Hansson 已提交
152
#
153 154 155 156 157 158
# 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:
159
#       self.use_instantiated_fixtures = :no_instances
160
#
161 162 163 164 165 166 167 168 169 170
# 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
#
171
# = Dynamic fixtures with ERb
D
Initial  
David Heinemeier Hansson 已提交
172
#
173 174
# 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:
D
Initial  
David Heinemeier Hansson 已提交
175
#
176 177 178 179 180
# <% for i in 1..1000 %>
# fix_<%= i %>:
#   id: <%= i %>
#   name: guy_<%= 1 %>
# <% end %>
D
Initial  
David Heinemeier Hansson 已提交
181
#
D
David Heinemeier Hansson 已提交
182
# This will create 1000 very simple YAML fixtures.
183 184
#
# Using ERb, you can also inject dynamic values into your fixtures with inserts like <%= Date.today.strftime("%Y-%m-%d") %>.
185
# This is however a feature to be used with some caution. The point of fixtures are that they're stable units of predictable
186 187
# 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.
188 189 190
#
# = Transactional fixtures
#
191
# TestCases can use begin+rollback to isolate their changes to the database instead of having to delete+insert for every test case.
192 193 194 195 196
# 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
197
#   
198
#     fixtures :foos
199
#   
200
#     def test_godzilla
201
#       assert !Foo.find(:all).empty?
202
#       Foo.destroy_all
203
#       assert Foo.find(:all).empty?
204
#     end
205
#   
206
#     def test_godzilla_aftermath
207
#       assert !Foo.find(:all).empty?
208 209
#     end
#   end
210
#   
211
# If you preload your test database with all fixture data (probably in the Rakefile task) and use transactional fixtures,
212 213
# 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.
#
214
# In order to use instantiated fixtures with preloaded data, set +self.pre_loaded_fixtures+ to true. This will provide
215 216
# access to fixture data for every table that has been loaded through fixtures (depending on the value of +use_instantiated_fixtures+)
#
217 218 219
# 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
220
#      the results of your transaction until Active Record supports nested transactions or savepoints (in progress).
221
#   2. Your database does not support transactions. Every Active Record database supports transactions except MySQL MyISAM.
222
#      Use InnoDB, MaxDB, or NDB instead.
223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267
#
# = 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
268
# any fixture from its label, you can specify FK's by label instead of ID.
269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308
#
# === 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).
#
309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333
# ==== 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.
#
334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440
# === 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.

441
class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash)
442 443
  DEFAULT_FILTER_RE = /\.ya?ml$/

444
  @@all_cached_fixtures = {}
445 446 447

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

451 452 453 454
  def self.cache_for_connection(connection)
    @@all_cached_fixtures[connection.object_id] ||= {}
    @@all_cached_fixtures[connection.object_id]
  end
455

456 457 458 459 460 461 462 463 464 465 466 467 468 469
  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)
470
    cache_for_connection(connection).update(fixtures.index_by { |f| f.table_name })
471
  end
472 473

  def self.instantiate_fixtures(object, table_name, fixtures, load_instances = true)
474
    object.instance_variable_set "@#{table_name.to_s.gsub('.','_')}", fixtures
475
    if load_instances
476
      ActiveRecord::Base.silence do
477
        fixtures.each do |name, fixture|
478 479 480 481
          begin
            object.instance_variable_set "@#{name}", fixture.find
          rescue FixtureClassNotFound
            nil
482
          end
483
        end
484
      end
D
Initial  
David Heinemeier Hansson 已提交
485 486
    end
  end
487

488
  def self.instantiate_all_loaded_fixtures(object, load_instances = true)
489 490
    all_loaded_fixtures.each do |table_name, fixtures|
      Fixtures.instantiate_fixtures(object, table_name, fixtures, load_instances)
491
    end
492
  end
493

494
  cattr_accessor :all_loaded_fixtures
495
  self.all_loaded_fixtures = {}
496

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

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

503 504
    unless table_names_to_fetch.empty?
      ActiveRecord::Base.silence do
505 506
        connection.disable_referential_integrity do
          fixtures_map = {}
507

508 509 510
          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
511

512
          all_loaded_fixtures.update(fixtures_map)
513

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

518 519 520 521 522
            # 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
523
            end
524
          end
525

526 527
          cache_fixtures(connection, fixtures)
        end
528
      end
529
    end
530
    cached_fixtures(connection, table_names)
531 532
  end

533 534 535 536 537 538 539
  # 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

540 541
  attr_reader :table_name

542
  def initialize(connection, table_name, class_name, fixture_path, file_filter = DEFAULT_FILTER_RE)
D
Initial  
David Heinemeier Hansson 已提交
543
    @connection, @table_name, @fixture_path, @file_filter = connection, table_name, fixture_path, file_filter
544
    @class_name = class_name ||
545
                  (ActiveRecord::Base.pluralize_table_names ? @table_name.singularize.camelize : @table_name.camelize)
546
    @table_name = ActiveRecord::Base.table_name_prefix + @table_name + ActiveRecord::Base.table_name_suffix
547 548
    @table_name = class_name.table_name if class_name.respond_to?(:table_name)
    @connection = class_name.connection if class_name.respond_to?(:connection)
D
Initial  
David Heinemeier Hansson 已提交
549
    read_fixture_files
550 551 552
  end

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

  def insert_fixtures
557 558 559 560 561 562 563 564 565 566 567 568 569 570
    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

571 572 573 574 575 576
      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
577 578 579 580 581 582 583
        end

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

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

589 590 591 592 593 594 595 596 597
        # 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|
598 599
          case association.macro
          when :belongs_to
600 601 602 603
            # 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)
604 605 606 607 608 609 610 611 612 613
              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

614 615 616 617 618 619 620 621 622
              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(
623
                  { association.primary_key_name => row[primary_key_name],
624 625 626 627 628 629 630
                    association.association_foreign_key => Fixtures.identify(target) }, nil)
              end
            end
          end
        end
      end

631
      @connection.insert_fixture(fixture, @table_name)
632
    end
633 634 635 636 637 638

    # insert any HABTM join tables we discovered
    habtm_fixtures.values.each do |fixture|
      fixture.delete_existing_fixtures
      fixture.insert_fixtures
    end
D
Initial  
David Heinemeier Hansson 已提交
639 640
  end

641
  private
642 643 644 645 646 647 648 649 650 651 652 653 654
    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

655 656 657
    def has_primary_key_column?
      @has_primary_key_column ||= model_class && primary_key_name &&
        model_class.columns.find { |c| c.name == primary_key_name }
658
    end
659

660 661 662 663 664 665
    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

666 667 668 669
    def inheritance_column_name
      @inheritance_column_name ||= model_class && model_class.inheritance_column
    end

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

D
Initial  
David Heinemeier Hansson 已提交
674
    def read_fixture_files
675
      if File.file?(yaml_file_path)
676
        read_yaml_fixture_files
677
      elsif File.file?(csv_file_path)
678
        read_csv_fixture_files
D
Initial  
David Heinemeier Hansson 已提交
679
      else
680
        # Standard fixtures
681 682 683 684 685
        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
D
Initial  
David Heinemeier Hansson 已提交
686 687 688 689
        end
      end
    end

690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715
    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
716 717
    end

718 719 720 721 722 723 724 725 726 727 728 729 730
    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"
D
Initial  
David Heinemeier Hansson 已提交
731
    end
732 733 734 735

    def csv_file_path
      @fixture_path + ".csv"
    end
736

D
Initial  
David Heinemeier Hansson 已提交
737 738 739 740
    def yaml_fixtures_key(path)
      File.basename(@fixture_path).split(".").first
    end

741 742 743 744 745 746
    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

D
Initial  
David Heinemeier Hansson 已提交
747 748 749 750 751 752 753
    def erb_render(fixture_content)
      ERB.new(fixture_content).result
    end
end

class Fixture #:nodoc:
  include Enumerable
754 755

  class FixtureError < StandardError #:nodoc:
756
  end
757 758

  class FormatError < FixtureError #:nodoc:
759
  end
D
Initial  
David Heinemeier Hansson 已提交
760

761 762
  attr_reader :class_name

D
Initial  
David Heinemeier Hansson 已提交
763
  def initialize(fixture, class_name)
764 765 766 767 768 769
    case fixture
      when Hash, YAML::Omap
        @fixture = fixture
      when String
        @fixture = read_fixture_file(fixture)
      else
770
        raise ArgumentError, "Bad fixture argument #{fixture.inspect} during creation of #{class_name} fixture"
771 772
    end

D
Initial  
David Heinemeier Hansson 已提交
773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788
    @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
789 790
    columns = @fixture.keys.collect{ |column_name| ActiveRecord::Base.connection.quote_column_name(column_name) }
    columns.join(", ")
D
Initial  
David Heinemeier Hansson 已提交
791 792 793
  end

  def value_list
794 795 796
    klass = @class_name.constantize rescue nil

    list = @fixture.inject([]) do |fixtures, (key, value)|
797
      col = klass.columns_hash[key] if klass.respond_to?(:ancestors) && klass.ancestors.include?(ActiveRecord::Base)
798
      fixtures << ActiveRecord::Base.connection.quote(value, col).gsub('[^\]\\n', "\n").gsub('[^\]\\r', "\r")
799 800
    end
    list * ', '
D
Initial  
David Heinemeier Hansson 已提交
801
  end
802

D
Initial  
David Heinemeier Hansson 已提交
803
  def find
804 805
    klass = @class_name.is_a?(Class) ? @class_name : Object.const_get(@class_name) rescue nil
    if klass
806
      klass.find(self[klass.primary_key])
807 808
    else
      raise FixtureClassNotFound, "The class #{@class_name.inspect} was not found."
809
    end
D
Initial  
David Heinemeier Hansson 已提交
810
  end
811

D
Initial  
David Heinemeier Hansson 已提交
812 813 814 815
  private
    def read_fixture_file(fixture_file_path)
      IO.readlines(fixture_file_path).inject({}) do |fixture, line|
        # Mercifully skip empty lines.
816
        next if line =~ /^\s*$/
D
Initial  
David Heinemeier Hansson 已提交
817 818 819

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

        # Disallow duplicate keys to catch typos.
825
        raise FormatError, "#{fixture_file_path}: duplicate '#{key}' in fixture." if fixture[key]
D
Initial  
David Heinemeier Hansson 已提交
826 827 828 829 830 831
        fixture[key] = value.strip
        fixture
      end
    end
end

832 833
module Test #:nodoc:
  module Unit #:nodoc:
834
    class TestCase #:nodoc:
835 836 837 838 839 840
      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
841 842 843 844

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

847
      @@already_loaded_fixtures = {}
848
      self.fixture_class_names = {}
849

850 851 852
      def self.set_fixture_class(class_names = {})
        self.fixture_class_names = self.fixture_class_names.merge(class_names)
      end
853

854
      def self.fixtures(*table_names)
855 856 857 858 859 860 861
        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

862 863 864 865
        self.fixture_table_names |= table_names
        require_fixture_classes(table_names)
        setup_fixture_accessors(table_names)
      end
866

867 868
      def self.require_fixture_classes(table_names = nil)
        (table_names || fixture_table_names).each do |table_name|
869 870
          file_name = table_name.to_s
          file_name = file_name.singularize if ActiveRecord::Base.pluralize_table_names
871
          begin
872
            require_dependency file_name
873 874
          rescue LoadError
            # Let's hope the developer has included it himself
875 876
          end
        end
877
      end
878

879
      def self.setup_fixture_accessors(table_names = nil)
880
        (table_names || fixture_table_names).each do |table_name|
881 882 883 884
          table_name = table_name.to_s.tr('.', '_')

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

886
            @fixture_cache[table_name] ||= {}
887

888 889 890 891 892 893 894 895
            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
896
            end
897 898

            instances.size == 1 ? instances.first : instances
899 900
          end
        end
901
      end
902

903
      def self.uses_transaction(*methods)
J
Jeremy Kemper 已提交
904 905
        @uses_transaction = [] unless defined?(@uses_transaction)
        @uses_transaction.concat methods.map(&:to_s)
906
      end
907

908
      def self.uses_transaction?(method)
J
Jeremy Kemper 已提交
909 910
        @uses_transaction = [] unless defined?(@uses_transaction)
        @uses_transaction.include?(method.to_s)
911 912 913
      end

      def use_transactional_fixtures?
914 915
        use_transactional_fixtures &&
          !self.class.uses_transaction?(method_name)
916 917
      end

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

921
        if pre_loaded_fixtures && !use_transactional_fixtures
922
          raise RuntimeError, 'pre_loaded_fixtures requires use_transactional_fixtures'
923 924
        end

925
        @fixture_cache = {}
926

927
        # Load fixtures once and begin transaction.
928
        if use_transactional_fixtures?
929 930 931 932 933 934
          if @@already_loaded_fixtures[self.class]
            @loaded_fixtures = @@already_loaded_fixtures[self.class]
          else
            load_fixtures
            @@already_loaded_fixtures[self.class] = @loaded_fixtures
          end
935
          ActiveRecord::Base.send :increment_open_transactions
936 937 938
          ActiveRecord::Base.connection.begin_db_transaction
        # Load fixtures for every test.
        else
939
          Fixtures.reset_cache
940 941
          @@already_loaded_fixtures[self.class] = nil
          load_fixtures
942 943 944 945 946 947 948 949
        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
950
        return unless defined?(ActiveRecord::Base) && !ActiveRecord::Base.configurations.blank?
951

952 953
        unless use_transactional_fixtures?
          Fixtures.reset_cache
954
        end
955

956
        # Rollback changes if a transaction is active.
957
        if use_transactional_fixtures? && Thread.current['open_transactions'] != 0
958
          ActiveRecord::Base.connection.rollback_db_transaction
959
          Thread.current['open_transactions'] = 0
960
        end
R
Rick Olson 已提交
961
        ActiveRecord::Base.verify_active_connections!
D
Initial  
David Heinemeier Hansson 已提交
962
      end
963 964 965 966 967 968 969
      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
970
            define_method(:setup) do
971 972 973 974 975 976 977
              setup_with_fixtures
              setup_without_fixtures
            end
          end
        when 'teardown'
          unless method_defined?(:teardown_without_fixtures)
            alias_method :teardown_without_fixtures, :teardown
978
            define_method(:teardown) do
979 980 981
              teardown_without_fixtures
              teardown_with_fixtures
            end
982 983 984
          end
        end
      end
985

986
      private
987 988
        def load_fixtures
          @loaded_fixtures = {}
989
          fixtures = Fixtures.create_fixtures(fixture_path, fixture_table_names, fixture_class_names)
990 991 992
          unless fixtures.nil?
            if fixtures.instance_of?(Fixtures)
              @loaded_fixtures[fixtures.table_name] = fixtures
993
            else
994 995
              fixtures.each { |f| @loaded_fixtures[f.table_name] = f }
            end
996
          end
997
        end
998

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

1002
        def instantiate_fixtures
1003 1004 1005
          if pre_loaded_fixtures
            raise RuntimeError, 'Load fixtures before instantiating them.' if Fixtures.all_loaded_fixtures.empty?
            unless @@required_fixture_classes
1006
              self.class.require_fixture_classes Fixtures.all_loaded_fixtures.keys
1007 1008 1009
              @@required_fixture_classes = true
            end
            Fixtures.instantiate_all_loaded_fixtures(self, load_instances?)
1010
          else
1011 1012 1013 1014
            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
1015
          end
1016
        end
1017

1018 1019 1020
        def load_instances?
          use_instantiated_fixtures != :no_instances
        end
D
Initial  
David Heinemeier Hansson 已提交
1021
    end
1022
  end
1023
end