fixtures.rb 32.6 KB
Newer Older
D
Initial  
David Heinemeier Hansson 已提交
1 2
require 'erb'
require 'yaml'
3
require 'csv'
4
require 'zlib'
J
Joshua Peek 已提交
5
require 'active_support/dependencies'
6
require 'active_support/core_ext/array/wrap'
7
require 'active_support/core_ext/object/blank'
8
require 'active_support/core_ext/logger'
D
Initial  
David Heinemeier Hansson 已提交
9

10 11 12 13 14 15
if RUBY_VERSION < '1.9'
  module YAML #:nodoc:
    class Omap #:nodoc:
      def keys;   map { |k, v| k } end
      def values; map { |k, v| v } end
    end
16
  end
17 18
end

19 20 21 22 23 24
if defined? ActiveRecord
  class FixtureClassNotFound < ActiveRecord::ActiveRecordError #:nodoc:
  end
else
  class FixtureClassNotFound < StandardError #:nodoc:
  end
25 26
end

27 28
class FixturesFileNotFound < StandardError; end

P
Pratik Naik 已提交
29 30 31 32 33
# Fixtures are a way of organizing data that you want to test against; in short, sample data.
#
# = Fixture formats
#
# Fixtures come in 3 flavors:
D
Initial  
David Heinemeier Hansson 已提交
34
#
35 36 37
#   1.  YAML fixtures
#   2.  CSV fixtures
#   3.  Single-file fixtures
D
Initial  
David Heinemeier Hansson 已提交
38
#
P
Pratik Naik 已提交
39
# == YAML fixtures
D
Initial  
David Heinemeier Hansson 已提交
40
#
41
# This type of fixture is in YAML format and the preferred default. YAML is a file format which describes data structures
42
# in a non-verbose, human-readable format. It ships with Ruby 1.8.1+.
D
Initial  
David Heinemeier Hansson 已提交
43
#
44 45 46
# Unlike single-file fixtures, YAML fixtures are stored in a single file per model, which are placed
# in the directory appointed by <tt>ActiveSupport::TestCase.fixture_path=(path)</tt> (this is
# automatically configured for Rails, so you can just put your files in <tt><your-rails-app>/test/fixtures/</tt>).
47
# The fixture file ends with the <tt>.yml</tt> file extension (Rails example:
P
Pratik Naik 已提交
48
# <tt><your-rails-app>/test/fixtures/web_sites.yml</tt>). The format of a YAML fixture file looks like this:
D
Initial  
David Heinemeier Hansson 已提交
49
#
50 51 52 53
#   rubyonrails:
#     id: 1
#     name: Ruby on Rails
#     url: http://www.rubyonrails.org
D
Initial  
David Heinemeier Hansson 已提交
54
#
55 56 57 58 59
#   google:
#     id: 2
#     name: Google
#     url: http://www.google.com
#
60
# This YAML fixture file includes two fixtures.  Each YAML fixture (ie. record) is given a name and is followed by an
61
# indented list of key/value pairs in the "key: value" format.  Records are separated by a blank line for your viewing
62
# pleasure.
63
#
64
# Note that YAML fixtures are unordered. If you want ordered fixtures, use the omap YAML type.
65
# See http://yaml.org/type/omap.html
66 67 68 69 70 71 72 73 74 75 76 77 78
# 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
#
P
Pratik Naik 已提交
79
# == CSV fixtures
80
#
P
Pratik Naik 已提交
81
# Fixtures can also be kept in the Comma Separated Value (CSV) format. Akin to YAML fixtures, CSV fixtures are stored
P
Pratik Naik 已提交
82 83
# in a single file, but instead end with the <tt>.csv</tt> file extension
# (Rails example: <tt><your-rails-app>/test/fixtures/web_sites.csv</tt>).
84
#
85
# The format of this type of fixture file is much more compact than the others, but also a little harder to read by us
86
# humans.  The first line of the CSV file is a comma-separated list of field names.  The rest of the
87
# file is then comprised
88 89 90 91 92 93 94 95 96 97
# 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
98
# fixture names are automatically generated by deriving the class name of the fixture file and adding an incrementing
99 100 101
# 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".
#
102
# Most databases and spreadsheets support exporting to CSV format, so this is a great format for you to choose if you
103
# have existing data somewhere already.
104
#
P
Pratik Naik 已提交
105
# == Single-file fixtures
106
#
107
# This type of fixture was the original format for Active Record that has since been deprecated in
108
# favor of the YAML and CSV formats.
109 110
# 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>ActiveSupport::TestCase.fixture_path=(path)</tt> (this is automatically
111
# configured for Rails, so you can just put your files in <tt><your-rails-app>/test/fixtures/<your-model-name>/</tt> --
P
Pratik Naik 已提交
112
# like <tt><your-rails-app>/test/fixtures/web_sites/</tt> for the WebSite model).
113 114
#
# Each text file placed in this directory represents a "record".  Usually these types of fixtures are named without
115
# extensions, but if you are on a Windows machine, you might consider adding <tt>.txt</tt> as the extension.
116
# Here's what the above example might look like:
117 118 119 120 121 122 123 124 125 126 127 128
#
#   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
#
P
Pratik Naik 已提交
129
# = Using fixtures in testcases
130 131
#
# Since fixtures are a testing construct, we use them in our unit and functional tests.  There are two ways to use the
132
# fixtures, but first let's take a look at a sample unit test:
133
#
P
Pratik Naik 已提交
134
#   require 'test_helper'
135
#
136
#   class WebSiteTest < ActiveSupport::TestCase
P
Pratik Naik 已提交
137
#     test "web_site_count" do
138
#       assert_equal 2, WebSite.count
139
#     end
140
#   end
141
#
142
# By default, the <tt>test_helper module</tt> will load all of your fixtures into your test database,
143
# so this test will succeed.
P
Pratik Naik 已提交
144
# The testing environment will automatically load the all fixtures into the database before each test.
D
David Heinemeier Hansson 已提交
145
# To ensure consistent data, the environment deletes the fixtures before running the load.
146
#
P
Pratik Naik 已提交
147 148 149
# In addition to being available in the database, the fixture's data may also be accessed by
# using a special dynamic method, which has the same name as the model, and accepts the
# name of the fixture to instantiate:
150
#
P
Pratik Naik 已提交
151 152
#   test "find" do
#     assert_equal "Ruby on Rails", web_sites(:rubyonrails).name
D
Initial  
David Heinemeier Hansson 已提交
153 154
#   end
#
P
Pratik Naik 已提交
155
# Alternatively, you may enable auto-instantiation of the fixture data. For instance, take the following tests:
D
Initial  
David Heinemeier Hansson 已提交
156
#
P
Pratik Naik 已提交
157 158 159
#   test "find_alt_method_1" do
#     assert_equal "Ruby on Rails", @web_sites['rubyonrails']['name']
#   end
160
#
P
Pratik Naik 已提交
161 162 163
#   test "find_alt_method_2" do
#     assert_equal "Ruby on Rails", @rubyonrails.news
#   end
164
#
P
Pratik Naik 已提交
165 166
# In order to use these methods to access fixtured data within your testcases, you must specify one of the
# following in your <tt>ActiveSupport::TestCase</tt>-derived class:
167
#
P
Pratik Naik 已提交
168 169
# - to fully enable instantiated fixtures (enable alternate methods #1 and #2 above)
#     self.use_instantiated_fixtures = true
170
#
P
Pratik Naik 已提交
171 172
# - create only the hash for the fixtures, do not 'find' each instance (enable alternate method #1 only)
#     self.use_instantiated_fixtures = :no_instances
173
#
P
Pratik Naik 已提交
174 175 176
# Using either of these alternate methods incurs a performance hit, as the fixtured data must be fully
# traversed in the database to create the fixture hash and/or instance variables. This is expensive for
# large sets of fixtured data.
177
#
178
# = Dynamic fixtures with ERb
D
Initial  
David Heinemeier Hansson 已提交
179
#
180 181
# 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 已提交
182
#
P
Pratik Naik 已提交
183 184 185 186 187
#   <% for i in 1..1000 %>
#   fix_<%= i %>:
#     id: <%= i %>
#     name: guy_<%= 1 %>
#   <% end %>
D
Initial  
David Heinemeier Hansson 已提交
188
#
D
David Heinemeier Hansson 已提交
189
# This will create 1000 very simple YAML fixtures.
190
#
P
Pratik Naik 已提交
191
# Using ERb, you can also inject dynamic values into your fixtures with inserts like <tt><%= Date.today.strftime("%Y-%m-%d") %></tt>.
192 193 194
# 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
195
# in fixtures are to be considered a code smell.
196 197 198
#
# = Transactional fixtures
#
199
# TestCases can use begin+rollback to isolate their changes to the database instead of having to
200
# delete+insert for every test case.
201
#
202
#   class FooTest < ActiveSupport::TestCase
203
#     self.use_transactional_fixtures = true
204
#
P
Pratik Naik 已提交
205
#     test "godzilla" do
206
#       assert !Foo.find(:all).empty?
207
#       Foo.destroy_all
208
#       assert Foo.find(:all).empty?
209
#     end
210
#
P
Pratik Naik 已提交
211
#     test "godzilla aftermath" do
212
#       assert !Foo.find(:all).empty?
213 214
#     end
#   end
215
#
216
# If you preload your test database with all fixture data (probably in the Rakefile task) and use transactional fixtures,
217
# then you may omit all fixtures declarations in your test cases since all the data's already there
218
# and every case rolls back its changes.
219
#
220
# In order to use instantiated fixtures with preloaded data, set +self.pre_loaded_fixtures+ to true. This will provide
221
# access to fixture data for every table that has been loaded through fixtures (depending on the
222
# value of +use_instantiated_fixtures+)
223
#
224
# When *not* to use transactional fixtures:
P
Pratik Naik 已提交
225
#
226 227
# 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
228
#    and rolled back in teardown. Thus, you won't be able to verify
P
Pratik Naik 已提交
229 230 231
#    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.
232 233 234 235 236
#
# = Advanced YAML Fixtures
#
# YAML fixtures that don't specify an ID get some extra features:
#
P
Pratik Naik 已提交
237
# * Stable, autogenerated IDs
238 239 240 241 242 243
# * 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
#
P
Pratik Naik 已提交
244
# == Stable, autogenerated IDs
245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265
#
# 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
#
P
Pratik Naik 已提交
266
# Active Record looks at the fixture's model class, discovers the correct
267 268 269 270 271 272 273 274 275
# 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
P
Pratik Naik 已提交
276
# mention difficult to read. Since Active Record can figure out the ID of
277
# any fixture from its label, you can specify FK's by label instead of ID.
278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298
#
# === 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
P
Pratik Naik 已提交
299
# use labels instead of IDs:
300 301 302 303 304 305 306 307 308 309 310 311 312
#
#   ### in pirates.yml
#
#   reginald:
#     name: Reginald the Pirate
#     monkey: george
#
#   ### in monkeys.yml
#
#   george:
#     name: George the Monkey
#     pirate: reginald
#
P
Pratik Naik 已提交
313
# Pow! All is made clear. Active Record reflects on the fixture's model class,
314 315
# finds all the +belongs_to+ associations, and allows you to specify
# a target *label* for the *association* (monkey: george) rather than
P
Pratik Naik 已提交
316
# a target *id* for the *FK* (<tt>monkey_id: 1</tt>).
317
#
318 319 320
# ==== Polymorphic belongs_to
#
# Supporting polymorphic relationships is a little bit more complicated, since
P
Pratik Naik 已提交
321
# Active Record needs to know what type your association is pointing at. Something
322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340
# 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)
#
P
Pratik Naik 已提交
341
# Just provide the polymorphic target type and Active Record will take care of the rest.
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
# === has_and_belongs_to_many
#
# Time to give our monkey some fruit.
#
#   ### in monkeys.yml
#
#   george:
#     id: 1
#     name: George the Monkey
#
#   ### 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:
386
#     id: 1
387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402
#     name: George the Monkey
#     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
P
Pratik Naik 已提交
403
# of monkeys on each fruit. As with +belongs_to+, Active Record reflects on
404 405 406 407 408
# the fixture's model class and discovers the +has_and_belongs_to_many+
# associations.
#
# == Autofilled timestamp columns
#
P
Pratik Naik 已提交
409 410 411
# If your table/model specifies any of Active Record's
# standard timestamp columns (+created_at+, +created_on+, +updated_at+, +updated_on+),
# they will automatically be set to <tt>Time.now</tt>.
412 413 414 415 416 417 418 419 420 421 422 423
#
# 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
P
Pratik Naik 已提交
424
# to be able to get a hold of the identifier for a given label. ERB
425 426 427 428 429 430 431 432 433
# 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
P
Pratik Naik 已提交
434
# your <tt>database.yml</tt> file. You can use the same technique in your fixtures:
435 436 437 438 439 440 441 442 443 444 445 446 447 448
#
#   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.

449
class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash)
450
  MAX_ID = 2 ** 30 - 1
451 452
  DEFAULT_FILTER_RE = /\.ya?ml$/

453
  @@all_cached_fixtures = {}
454 455 456

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

460 461 462 463
  def self.cache_for_connection(connection)
    @@all_cached_fixtures[connection.object_id] ||= {}
    @@all_cached_fixtures[connection.object_id]
  end
464

465 466 467 468 469 470 471 472 473 474 475 476 477
  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

478 479
  def self.cache_fixtures(connection, fixtures_map)
    cache_for_connection(connection).update(fixtures_map)
480
  end
481 482

  def self.instantiate_fixtures(object, table_name, fixtures, load_instances = true)
483
    object.instance_variable_set "@#{table_name.to_s.gsub('.','_')}", fixtures
484
    if load_instances
485
      ActiveRecord::Base.silence do
486
        fixtures.each do |name, fixture|
487 488 489 490
          begin
            object.instance_variable_set "@#{name}", fixture.find
          rescue FixtureClassNotFound
            nil
491
          end
492
        end
493
      end
D
Initial  
David Heinemeier Hansson 已提交
494 495
    end
  end
496

497
  def self.instantiate_all_loaded_fixtures(object, load_instances = true)
498 499
    all_loaded_fixtures.each do |table_name, fixtures|
      Fixtures.instantiate_fixtures(object, table_name, fixtures, load_instances)
500
    end
501
  end
502

503
  cattr_accessor :all_loaded_fixtures
504
  self.all_loaded_fixtures = {}
505

506
  def self.create_fixtures(fixtures_directory, table_names, class_names = {})
507
    table_names = [table_names].flatten.map { |n| n.to_s }
508
    table_names.each { |n| class_names[n.tr('/', '_').to_sym] = n.classify if n.include?('/') }
509 510
    connection  = block_given? ? yield : ActiveRecord::Base.connection

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

513 514
    unless table_names_to_fetch.empty?
      ActiveRecord::Base.silence do
515 516
        connection.disable_referential_integrity do
          fixtures_map = {}
517

518
          fixtures = table_names_to_fetch.map do |table_name|
519
            fixtures_map[table_name] = Fixtures.new(connection, table_name.tr('/', '_'), class_names[table_name.tr('/', '_').to_sym], File.join(fixtures_directory, table_name))
520
          end
521

522
          all_loaded_fixtures.update(fixtures_map)
523

524
          connection.transaction(:requires_new => true) do
525 526
            fixtures.reverse.each { |fixture| fixture.delete_existing_fixtures }
            fixtures.each { |fixture| fixture.insert_fixtures }
527

528 529 530
            # Cap primary key sequences to max(pk).
            if connection.respond_to?(:reset_pk_sequence!)
              table_names.each do |table_name|
531
                connection.reset_pk_sequence!(table_name.tr('/', '_'))
532
              end
533
            end
534
          end
535

536
          cache_fixtures(connection, fixtures_map)
537
        end
538
      end
539
    end
540
    cached_fixtures(connection, table_names)
541 542
  end

543 544
  # Returns a consistent, platform-independent identifier for +label+.
  # Identifiers are positive integers less than 2^32.
545
  def self.identify(label)
546
    Zlib.crc32(label.to_s) % MAX_ID
547 548
  end

549
  attr_reader :table_name, :name
550

551
  def initialize(connection, table_name, class_name, fixture_path, file_filter = DEFAULT_FILTER_RE)
D
Initial  
David Heinemeier Hansson 已提交
552
    @connection, @table_name, @fixture_path, @file_filter = connection, table_name, fixture_path, file_filter
553
    @name = table_name # preserve fixture base name
554
    @class_name = class_name ||
555
                  (ActiveRecord::Base.pluralize_table_names ? @table_name.singularize.camelize : @table_name.camelize)
556
    @table_name = "#{ActiveRecord::Base.table_name_prefix}#{@table_name}#{ActiveRecord::Base.table_name_suffix}"
557 558
    @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 已提交
559
    read_fixture_files
560 561 562
  end

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

  def insert_fixtures
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
571 572 573 574 575
    if is_a?(Hash)
      delete('DEFAULTS')
    else
      delete(assoc('DEFAULTS'))
    end
576 577 578 579 580 581 582 583 584

    # 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

585 586 587 588 589 590
      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
591 592 593 594 595 596 597
        end

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

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

603 604 605 606 607 608 609 610 611
        # 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|
612 613
          case association.macro
          when :belongs_to
614 615 616 617
            # 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)
618 619 620 621 622 623 624 625 626 627
              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

628 629 630 631 632 633 634 635 636
              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(
637
                  { association.foreign_key             => row[primary_key_name],
638 639
                    association.association_foreign_key => Fixtures.identify(target) },
                  nil, @connection)
640 641 642 643 644 645
              end
            end
          end
        end
      end

646
      @connection.insert_fixture(fixture, @table_name)
647
    end
648 649 650 651 652 653

    # 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 已提交
654 655
  end

656
  private
657 658 659 660 661
    class HabtmFixtures < ::Fixtures #:nodoc:
      def read_fixture_files; end
    end

    def model_class
662 663 664 665 666 667 668 669 670 671
      unless defined?(@model_class)
        @model_class =
          if @class_name.nil? || @class_name.is_a?(Class)
            @class_name
          else
            @class_name.constantize rescue nil
          end
      end

      @model_class
672 673 674 675 676 677
    end

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

678
    def has_primary_key_column?
679 680
      @has_primary_key_column ||= primary_key_name &&
        model_class.columns.any? { |c| c.name == primary_key_name }
681
    end
682

683
    def timestamp_column_names
684 685
      @timestamp_column_names ||=
        %w(created_at created_on updated_at updated_on) & column_names
686 687
    end

688 689 690 691
    def inheritance_column_name
      @inheritance_column_name ||= model_class && model_class.inheritance_column
    end

692
    def column_names
693
      @column_names ||= @connection.columns(@table_name).collect { |c| c.name }
694 695
    end

D
Initial  
David Heinemeier Hansson 已提交
696
    def read_fixture_files
697
      if File.file?(yaml_file_path)
698
        read_yaml_fixture_files
699
      elsif File.file?(csv_file_path)
700
        read_csv_fixture_files
701 702
      else
        raise FixturesFileNotFound, "Could not find #{yaml_file_path} or #{csv_file_path}"
D
Initial  
David Heinemeier Hansson 已提交
703 704 705
      end
    end

706
    def read_yaml_fixture_files
707 708 709
      yaml_string = (Dir["#{@fixture_path}/**/*.yml"].select { |f|
        File.file?(f)
      } + [yaml_file_path]).map { |file_path| IO.read(file_path) }.join
710 711 712 713 714 715 716 717 718 719 720

      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|
721
          raise Fixture::FormatError, "Bad data for #{@class_name} fixture named #{fixture}" unless fixture.respond_to?(:each)
722
          fixture.each do |name, data|
723 724 725 726
            unless data
              raise Fixture::FormatError, "Bad data for #{@class_name} fixture named #{name} (nil)"
            end

727
            self[name] = Fixture.new(data, model_class, @connection)
728 729 730
          end
        end
      end
731 732
    end

733
    def read_csv_fixture_files
734
      reader = CSV.parse(erb_render(IO.read(csv_file_path)))
735 736 737 738 739
      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 }
740
        self["#{@class_name.to_s.underscore}_#{i+=1}"] = Fixture.new(data, model_class, @connection)
741 742 743 744 745
      end
    end

    def yaml_file_path
      "#{@fixture_path}.yml"
D
Initial  
David Heinemeier Hansson 已提交
746
    end
747 748 749 750

    def csv_file_path
      @fixture_path + ".csv"
    end
751

D
Initial  
David Heinemeier Hansson 已提交
752 753 754 755
    def yaml_fixtures_key(path)
      File.basename(@fixture_path).split(".").first
    end

756 757 758 759 760 761
    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 已提交
762 763 764 765 766 767 768
    def erb_render(fixture_content)
      ERB.new(fixture_content).result
    end
end

class Fixture #:nodoc:
  include Enumerable
769 770

  class FixtureError < StandardError #:nodoc:
771
  end
772 773

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

776
  attr_reader :model_class
777

778 779
  def initialize(fixture, model_class, connection = ActiveRecord::Base.connection)
    @connection = connection
780
    @fixture = fixture
781 782 783 784 785
    @model_class = model_class.is_a?(Class) ? model_class : model_class.constantize rescue nil
  end

  def class_name
    @model_class.name if @model_class
D
Initial  
David Heinemeier Hansson 已提交
786 787 788 789 790 791 792 793 794 795 796 797 798 799 800
  end

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

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

  def to_hash
    @fixture
  end

  def key_list
801
    @fixture.keys.map { |column_name| @connection.quote_column_name(column_name) }.join(', ')
D
Initial  
David Heinemeier Hansson 已提交
802 803 804
  end

  def value_list
805 806 807 808
    cols = (model_class && model_class < ActiveRecord::Base) ? model_class.columns_hash : {}
    @fixture.map do |key, value|
      @connection.quote(value, cols[key]).gsub('[^\]\\n', "\n").gsub('[^\]\\r', "\r")
    end.join(', ')
D
Initial  
David Heinemeier Hansson 已提交
809
  end
810

D
Initial  
David Heinemeier Hansson 已提交
811
  def find
812 813
    if model_class
      model_class.find(self[model_class.primary_key])
814
    else
815
      raise FixtureClassNotFound, "No class attached to find."
816
    end
D
Initial  
David Heinemeier Hansson 已提交
817 818 819
  end
end

820 821
module ActiveRecord
  module TestFixtures
822
    extend ActiveSupport::Concern
823 824 825 826 827

    included do
      setup :setup_fixtures
      teardown :teardown_fixtures

828 829 830 831 832 833
      class_attribute :fixture_path
      class_attribute :fixture_table_names
      class_attribute :fixture_class_names
      class_attribute :use_transactional_fixtures
      class_attribute :use_instantiated_fixtures   # true, false, or :no_instances
      class_attribute :pre_loaded_fixtures
834 835

      self.fixture_table_names = []
836 837
      self.use_transactional_fixtures = true
      self.use_instantiated_fixtures = false
838
      self.pre_loaded_fixtures = false
839

840
      self.fixture_class_names = {}
841 842 843 844 845 846
    end

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

848 849
      def fixtures(*table_names)
        if table_names.first == :all
850 851
          table_names = Dir["#{fixture_path}/**/*.{yml,csv}"]
          table_names.map! { |f| f[(fixture_path.size + 1)..-5] }
852 853
        else
          table_names = table_names.flatten.map { |n| n.to_s }
854 855
        end

856 857 858 859 860 861 862 863 864
        self.fixture_table_names |= table_names
        require_fixture_classes(table_names)
        setup_fixture_accessors(table_names)
      end

      def try_to_load_dependency(file_name)
        require_dependency file_name
      rescue LoadError => e
        # Let's hope the developer has included it himself
865

866 867 868 869
        # Let's warn in case this is a subdependency, otherwise
        # subdependency error messages are totally cryptic
        if ActiveRecord::Base.logger
          ActiveRecord::Base.logger.warn("Unable to load #{file_name}, underlying cause #{e.message} \n\n #{e.backtrace.join("\n")}")
870
        end
871
      end
872

873 874 875 876 877
      def 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
          try_to_load_dependency(file_name)
878
        end
879
      end
880

881
      def setup_fixture_accessors(table_names = nil)
882 883
        table_names = Array.wrap(table_names || fixture_table_names)
        table_names.each do |table_name|
884
          table_name = table_name.to_s.tr('./', '_')
885

886
          redefine_method(table_name) do |*fixtures|
887
            force_reload = fixtures.pop if fixtures.last == true || fixtures.last == :reload
888

889
            @fixture_cache[table_name] ||= {}
890

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

894 895 896 897
              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}'"
898
              end
899
            end
900

901 902
            instances.size == 1 ? instances.first : instances
          end
903
          private table_name
904
        end
905
      end
906

907 908
      def uses_transaction(*methods)
        @uses_transaction = [] unless defined?(@uses_transaction)
909
        @uses_transaction.concat methods.map { |m| m.to_s }
910 911
      end

912 913 914
      def uses_transaction?(method)
        @uses_transaction = [] unless defined?(@uses_transaction)
        @uses_transaction.include?(method.to_s)
915
      end
916
    end
917

918
    def run_in_transaction?
919 920 921
      use_transactional_fixtures &&
        !self.class.uses_transaction?(method_name)
    end
922

923 924
    def setup_fixtures
      return unless defined?(ActiveRecord) && !ActiveRecord::Base.configurations.blank?
925

926 927 928
      if pre_loaded_fixtures && !use_transactional_fixtures
        raise RuntimeError, 'pre_loaded_fixtures requires use_transactional_fixtures'
      end
929

930
      @fixture_cache = {}
931
      @@already_loaded_fixtures ||= {}
932 933

      # Load fixtures once and begin transaction.
934
      if run_in_transaction?
935 936
        if @@already_loaded_fixtures[self.class]
          @loaded_fixtures = @@already_loaded_fixtures[self.class]
937
        else
938
          load_fixtures
939
          @@already_loaded_fixtures[self.class] = @loaded_fixtures
940
        end
941
        ActiveRecord::Base.connection.increment_open_transactions
942
        ActiveRecord::Base.connection.transaction_joinable = false
943 944 945 946 947 948
        ActiveRecord::Base.connection.begin_db_transaction
      # Load fixtures for every test.
      else
        Fixtures.reset_cache
        @@already_loaded_fixtures[self.class] = nil
        load_fixtures
949 950
      end

951 952 953 954 955 956
      # Instantiate fixtures for every test if requested.
      instantiate_fixtures if use_instantiated_fixtures
    end

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

958
      unless run_in_transaction?
959 960
        Fixtures.reset_cache
      end
961

962
      # Rollback changes if a transaction is active.
963
      if run_in_transaction? && ActiveRecord::Base.connection.open_transactions != 0
964 965
        ActiveRecord::Base.connection.rollback_db_transaction
        ActiveRecord::Base.connection.decrement_open_transactions
D
Initial  
David Heinemeier Hansson 已提交
966
      end
967 968
      ActiveRecord::Base.clear_active_connections!
    end
969

970 971 972 973 974 975 976 977 978
    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.name] = fixtures
          else
            fixtures.each { |f| @loaded_fixtures[f.name] = f }
979
          end
980
        end
981
      end
982

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

986 987 988 989 990 991 992 993 994 995 996 997
      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?)
998
          end
999
        end
1000
      end
1001

1002 1003 1004
      def load_instances?
        use_instantiated_fixtures != :no_instances
      end
1005
  end
1006
end