Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

root/trunk/activerecord/lib/active_record/fixtures.rb

Revision 9191, 33.5 kB (checked in by bitsweat, 2 years ago)

Ruby 1.9 compat: delete DEFAULTS key from Hash not Omap array

  • Property svn:executable set to *
Line 
1 require 'erb'
2 require 'yaml'
3 require 'csv'
4 require 'active_support/test_case'
5
6 if RUBY_VERSION < '1.9'
7   module YAML #:nodoc:
8     class Omap #:nodoc:
9       def keys;   map { |k, v| k } end
10       def values; map { |k, v| v } end
11     end
12   end
13 end
14
15 if defined? ActiveRecord
16   class FixtureClassNotFound < ActiveRecord::ActiveRecordError #:nodoc:
17   end
18 else
19   class FixtureClassNotFound < StandardError #:nodoc:
20   end
21 end
22
23 # Fixtures are a way of organizing data that you want to test against; in short, sample data. They come in 3 flavors:
24 #
25 #   1.  YAML fixtures
26 #   2.  CSV fixtures
27 #   3.  Single-file fixtures
28 #
29 # = YAML fixtures
30 #
31 # This type of fixture is in YAML format and the preferred default. YAML is a file format which describes data structures
32 # in a non-verbose, human-readable format. It ships with Ruby 1.8.1+.
33 #
34 # Unlike single-file fixtures, YAML fixtures are stored in a single file per model, which are placed in the directory appointed
35 # by <tt>ActiveSupport::TestCase.fixture_path=(path)</tt> (this is automatically configured for Rails, so you can just
36 # put your files in <your-rails-app>/test/fixtures/). The fixture file ends with the .yml file extension (Rails example:
37 # "<your-rails-app>/test/fixtures/web_sites.yml"). The format of a YAML fixture file looks like this:
38 #
39 #   rubyonrails:
40 #     id: 1
41 #     name: Ruby on Rails
42 #     url: http://www.rubyonrails.org
43 #
44 #   google:
45 #     id: 2
46 #     name: Google
47 #     url: http://www.google.com
48 #
49 # This YAML fixture file includes two fixtures.  Each YAML fixture (ie. record) is given a name and is followed by an
50 # indented list of key/value pairs in the "key: value" format.  Records are separated by a blank line for your viewing
51 # pleasure.
52 #
53 # Note that YAML fixtures are unordered. If you want ordered fixtures, use the omap YAML type.  See http://yaml.org/type/omap.html
54 # for the specification.  You will need ordered fixtures when you have foreign key constraints on keys in the same table.
55 # This is commonly needed for tree structures.  Example:
56 #
57 #    --- !omap
58 #    - parent:
59 #        id:         1
60 #        parent_id:  NULL
61 #        title:      Parent
62 #    - child:
63 #        id:         2
64 #        parent_id:  1
65 #        title:      Child
66 #
67 # = CSV fixtures
68 #
69 # Fixtures can also be kept in the Comma Separated Value format. Akin to YAML fixtures, CSV fixtures are stored
70 # in a single file, but instead end with the .csv file extension (Rails example: "<your-rails-app>/test/fixtures/web_sites.csv")
71 #
72 # The format of this type of fixture file is much more compact than the others, but also a little harder to read by us
73 # humans.  The first line of the CSV file is a comma-separated list of field names.  The rest of the file is then comprised
74 # of the actual data (1 per line).  Here's an example:
75 #
76 #   id, name, url
77 #   1, Ruby On Rails, http://www.rubyonrails.org
78 #   2, Google, http://www.google.com
79 #
80 # Should you have a piece of data with a comma character in it, you can place double quotes around that value.  If you
81 # need to use a double quote character, you must escape it with another double quote.
82 #
83 # Another unique attribute of the CSV fixture is that it has *no* fixture name like the other two formats.  Instead, the
84 # fixture names are automatically generated by deriving the class name of the fixture file and adding an incrementing
85 # number to the end.  In our example, the 1st fixture would be called "web_site_1" and the 2nd one would be called
86 # "web_site_2".
87 #
88 # Most databases and spreadsheets support exporting to CSV format, so this is a great format for you to choose if you
89 # have existing data somewhere already.
90 #
91 # = Single-file fixtures
92 #
93 # This type of fixture was the original format for Active Record that has since been deprecated in favor of the YAML and CSV formats.
94 # Fixtures for this format are created by placing text files in a sub-directory (with the name of the model) to the directory
95 # appointed by <tt>ActiveSupport::TestCase.fixture_path=(path)</tt> (this is automatically configured for Rails, so you can just
96 # put your files in <your-rails-app>/test/fixtures/<your-model-name>/ -- like <your-rails-app>/test/fixtures/web_sites/ for the WebSite
97 # model).
98 #
99 # Each text file placed in this directory represents a "record".  Usually these types of fixtures are named without
100 # extensions, but if you are on a Windows machine, you might consider adding .txt as the extension.  Here's what the
101 # above example might look like:
102 #
103 #   web_sites/google
104 #   web_sites/yahoo.txt
105 #   web_sites/ruby-on-rails
106 #
107 # The file format of a standard fixture is simple.  Each line is a property (or column in db speak) and has the syntax
108 # of "name => value".  Here's an example of the ruby-on-rails fixture above:
109 #
110 #   id => 1
111 #   name => Ruby on Rails
112 #   url => http://www.rubyonrails.org
113 #
114 # = Using Fixtures
115 #
116 # Since fixtures are a testing construct, we use them in our unit and functional tests.  There are two ways to use the
117 # fixtures, but first let's take a look at a sample unit test:
118 #
119 #   require 'web_site'
120 #
121 #   class WebSiteTest < ActiveSupport::TestCase
122 #     def test_web_site_count
123 #       assert_equal 2, WebSite.count
124 #     end
125 #   end
126 #
127 # As it stands, unless we pre-load the web_site table in our database with two records, this test will fail.  Here's the
128 # easiest way to add fixtures to the database:
129 #
130 #   ...
131 #   class WebSiteTest < ActiveSupport::TestCase
132 #     fixtures :web_sites # add more by separating the symbols with commas
133 #   ...
134 #
135 # By adding a "fixtures" method to the test case and passing it a list of symbols (only one is shown here though), we trigger
136 # the testing environment to automatically load the appropriate fixtures into the database before each test.
137 # To ensure consistent data, the environment deletes the fixtures before running the load.
138 #
139 # In addition to being available in the database, the fixtures are also loaded into a hash stored in an instance variable
140 # of the test case.  It is named after the symbol... so, in our example, there would be a hash available called
141 # @web_sites.  This is where the "fixture name" comes into play.
142 #
143 # On top of that, each record is automatically "found" (using Model.find(id)) and placed in the instance variable of its name.
144 # So for the YAML fixtures, we'd get @rubyonrails and @google, which could be interrogated using regular Active Record semantics:
145 #
146 #   # test if the object created from the fixture data has the same attributes as the data itself
147 #   def test_find
148 #     assert_equal @web_sites["rubyonrails"]["name"], @rubyonrails.name
149 #   end
150 #
151 # As seen above, the data hash created from the YAML fixtures would have @web_sites["rubyonrails"]["url"] return
152 # "http://www.rubyonrails.org" and @web_sites["google"]["name"] would return "Google". The same fixtures, but loaded
153 # from a CSV fixture file, would be accessible via @web_sites["web_site_1"]["name"] == "Ruby on Rails" and have the individual
154 # fixtures available as instance variables @web_site_1 and @web_site_2.
155 #
156 # If you do not wish to use instantiated fixtures (usually for performance reasons) there are two options.
157 #
158 #   - to completely disable instantiated fixtures:
159 #       self.use_instantiated_fixtures = false
160 #
161 #   - to keep the fixture instance (@web_sites) available, but do not automatically 'find' each instance:
162 #       self.use_instantiated_fixtures = :no_instances
163 #
164 # Even if auto-instantiated fixtures are disabled, you can still access them
165 # by name via special dynamic methods. Each method has the same name as the
166 # model, and accepts the name of the fixture to instantiate:
167 #
168 #   fixtures :web_sites
169 #
170 #   def test_find
171 #     assert_equal "Ruby on Rails", web_sites(:rubyonrails).name
172 #   end
173 #
174 # = Dynamic fixtures with ERb
175 #
176 # 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
177 # mix ERb in with your YAML or CSV fixtures to create a bunch of fixtures for load testing, like:
178 #
179 #   <% for i in 1..1000 %>
180 #   fix_<%= i %>:
181 #     id: <%= i %>
182 #     name: guy_<%= 1 %>
183 #   <% end %>
184 #
185 # This will create 1000 very simple YAML fixtures.
186 #
187 # Using ERb, you can also inject dynamic values into your fixtures with inserts like <%= Date.today.strftime("%Y-%m-%d") %>.
188 # This is however a feature to be used with some caution. The point of fixtures are that they're stable units of predictable
189 # sample data. If you feel that you need to inject dynamic values, then perhaps you should reexamine whether your application
190 # is properly testable. Hence, dynamic values in fixtures are to be considered a code smell.
191 #
192 # = Transactional fixtures
193 #
194 # TestCases can use begin+rollback to isolate their changes to the database instead of having to delete+insert for every test case.
195 # They can also turn off auto-instantiation of fixture data since the feature is costly and often unused.
196 #
197 #   class FooTest < ActiveSupport::TestCase
198 #     self.use_transactional_fixtures = true
199 #     self.use_instantiated_fixtures = false
200 #   
201 #     fixtures :foos
202 #   
203 #     def test_godzilla
204 #       assert !Foo.find(:all).empty?
205 #       Foo.destroy_all
206 #       assert Foo.find(:all).empty?
207 #     end
208 #   
209 #     def test_godzilla_aftermath
210 #       assert !Foo.find(:all).empty?
211 #     end
212 #   end
213 #   
214 # If you preload your test database with all fixture data (probably in the Rakefile task) and use transactional fixtures,
215 # 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.
216 #
217 # In order to use instantiated fixtures with preloaded data, set +self.pre_loaded_fixtures+ to true. This will provide
218 # access to fixture data for every table that has been loaded through fixtures (depending on the value of +use_instantiated_fixtures+)
219 #
220 # When *not* to use transactional fixtures:
221 #   1. You're testing whether a transaction works correctly. Nested transactions don't commit until all parent transactions commit,
222 #      particularly, the fixtures transaction which is begun in setup and rolled back in teardown. Thus, you won't be able to verify
223 #      the results of your transaction until Active Record supports nested transactions or savepoints (in progress).
224 #   2. Your database does not support transactions. Every Active Record database supports transactions except MySQL MyISAM.
225 #      Use InnoDB, MaxDB, or NDB instead.
226 #
227 # = Advanced YAML Fixtures
228 #
229 # YAML fixtures that don't specify an ID get some extra features:
230 #
231 # * Stable, autogenerated ID's
232 # * Label references for associations (belongs_to, has_one, has_many)
233 # * HABTM associations as inline lists
234 # * Autofilled timestamp columns
235 # * Fixture label interpolation
236 # * Support for YAML defaults
237 #
238 # == Stable, autogenerated ID's
239 #
240 # Here, have a monkey fixture:
241 #
242 #   george:
243 #     id: 1
244 #     name: George the Monkey
245 #
246 #   reginald:
247 #     id: 2
248 #     name: Reginald the Pirate
249 #
250 # Each of these fixtures has two unique identifiers: one for the database
251 # and one for the humans. Why don't we generate the primary key instead?
252 # Hashing each fixture's label yields a consistent ID:
253 #
254 #   george: # generated id: 503576764
255 #     name: George the Monkey
256 #
257 #   reginald: # generated id: 324201669
258 #     name: Reginald the Pirate
259 #
260 # ActiveRecord looks at the fixture's model class, discovers the correct
261 # primary key, and generates it right before inserting the fixture
262 # into the database.
263 #
264 # The generated ID for a given label is constant, so we can discover
265 # any fixture's ID without loading anything, as long as we know the label.
266 #
267 # == Label references for associations (belongs_to, has_one, has_many)
268 #
269 # Specifying foreign keys in fixtures can be very fragile, not to
270 # mention difficult to read. Since ActiveRecord can figure out the ID of
271 # any fixture from its label, you can specify FK's by label instead of ID.
272 #
273 # === belongs_to
274 #
275 # Let's break out some more monkeys and pirates.
276 #
277 #   ### in pirates.yml
278 #
279 #   reginald:
280 #     id: 1
281 #     name: Reginald the Pirate
282 #     monkey_id: 1
283 #
284 #   ### in monkeys.yml
285 #
286 #   george:
287 #     id: 1
288 #     name: George the Monkey
289 #     pirate_id: 1
290 #
291 # Add a few more monkeys and pirates and break this into multiple files,
292 # and it gets pretty hard to keep track of what's going on. Let's
293 # use labels instead of ID's:
294 #
295 #   ### in pirates.yml
296 #
297 #   reginald:
298 #     name: Reginald the Pirate
299 #     monkey: george
300 #
301 #   ### in monkeys.yml
302 #
303 #   george:
304 #     name: George the Monkey
305 #     pirate: reginald
306 #
307 # Pow! All is made clear. ActiveRecord reflects on the fixture's model class,
308 # finds all the +belongs_to+ associations, and allows you to specify
309 # a target *label* for the *association* (monkey: george) rather than
310 # a target *id* for the *FK* (monkey_id: 1).
311 #
312 # ==== Polymorphic belongs_to
313 #
314 # Supporting polymorphic relationships is a little bit more complicated, since
315 # ActiveRecord needs to know what type your association is pointing at. Something
316 # like this should look familiar:
317 #
318 #   ### in fruit.rb
319 #
320 #   belongs_to :eater, :polymorphic => true
321 #
322 #   ### in fruits.yml
323 #
324 #   apple:
325 #     id: 1
326 #     name: apple
327 #     eater_id: 1
328 #     eater_type: Monkey
329 #
330 # Can we do better? You bet!
331 #
332 #   apple:
333 #     eater: george (Monkey)
334 #
335 # Just provide the polymorphic target type and ActiveRecord will take care of the rest.
336 #
337 # === has_and_belongs_to_many
338 #
339 # Time to give our monkey some fruit.
340 #
341 #   ### in monkeys.yml
342 #
343 #   george:
344 #     id: 1
345 #     name: George the Monkey
346 #     pirate_id: 1
347 #
348 #   ### in fruits.yml
349 #
350 #   apple:
351 #     id: 1
352 #     name: apple
353 #
354 #   orange:
355 #     id: 2
356 #     name: orange
357 #
358 #   grape:
359 #     id: 3
360 #     name: grape
361 #
362 #   ### in fruits_monkeys.yml
363 #
364 #   apple_george:
365 #     fruit_id: 1
366 #     monkey_id: 1
367 #
368 #   orange_george:
369 #     fruit_id: 2
370 #     monkey_id: 1
371 #
372 #   grape_george:
373 #     fruit_id: 3
374 #     monkey_id: 1
375 #
376 # Let's make the HABTM fixture go away.
377 #
378 #   ### in monkeys.yml
379 #
380 #   george:
381 #     name: George the Monkey
382 #     pirate: reginald
383 #     fruits: apple, orange, grape
384 #
385 #   ### in fruits.yml
386 #
387 #   apple:
388 #     name: apple
389 #
390 #   orange:
391 #     name: orange
392 #
393 #   grape:
394 #     name: grape
395 #
396 # Zap! No more fruits_monkeys.yml file. We've specified the list of fruits
397 # on George's fixture, but we could've just as easily specified a list
398 # of monkeys on each fruit. As with +belongs_to+, ActiveRecord reflects on
399 # the fixture's model class and discovers the +has_and_belongs_to_many+
400 # associations.
401 #
402 # == Autofilled timestamp columns
403 #
404 # If your table/model specifies any of ActiveRecord's
405 # standard timestamp columns (created_at, created_on, updated_at, updated_on),
406 # they will automatically be set to Time.now.
407 #
408 # If you've set specific values, they'll be left alone.
409 #
410 # == Fixture label interpolation
411 #
412 # The label of the current fixture is always available as a column value:
413 #
414 #   geeksomnia:
415 #     name: Geeksomnia's Account
416 #     subdomain: $LABEL
417 #
418 # Also, sometimes (like when porting older join table fixtures) you'll need
419 # to be able to get ahold of the identifier for a given label. ERB
420 # to the rescue:
421 #
422 #   george_reginald:
423 #     monkey_id: <%= Fixtures.identify(:reginald) %>
424 #     pirate_id: <%= Fixtures.identify(:george) %>
425 #
426 # == Support for YAML defaults
427 #
428 # You probably already know how to use YAML to set and reuse defaults in
429 # your +database.yml+ file,. You can use the same technique in your fixtures:
430 #
431 #   DEFAULTS: &DEFAULTS
432 #     created_on: <%= 3.weeks.ago.to_s(:db) %>
433 #
434 #   first:
435 #     name: Smurf
436 #     <<: *DEFAULTS
437 #
438 #   second:
439 #     name: Fraggle
440 #     <<: *DEFAULTS
441 #
442 # Any fixture labeled "DEFAULTS" is safely ignored.
443
444 class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash)
445   DEFAULT_FILTER_RE = /\.ya?ml$/
446
447   @@all_cached_fixtures = {}
448
449   def self.reset_cache(connection = nil)
450     connection ||= ActiveRecord::Base.connection
451     @@all_cached_fixtures[connection.object_id] = {}
452   end
453
454   def self.cache_for_connection(connection)
455     @@all_cached_fixtures[connection.object_id] ||= {}
456     @@all_cached_fixtures[connection.object_id]
457   end
458
459   def self.fixture_is_cached?(connection, table_name)
460     cache_for_connection(connection)[table_name]
461   end
462
463   def self.cached_fixtures(connection, keys_to_fetch = nil)
464     if keys_to_fetch
465       fixtures = cache_for_connection(connection).values_at(*keys_to_fetch)
466     else
467       fixtures = cache_for_connection(connection).values
468     end
469     fixtures.size > 1 ? fixtures : fixtures.first
470   end
471
472   def self.cache_fixtures(connection, fixtures)
473     cache_for_connection(connection).update(fixtures.index_by { |f| f.table_name })
474   end
475
476   def self.instantiate_fixtures(object, table_name, fixtures, load_instances = true)
477     object.instance_variable_set "@#{table_name.to_s.gsub('.','_')}", fixtures
478     if load_instances
479       ActiveRecord::Base.silence do
480         fixtures.each do |name, fixture|
481           begin
482             object.instance_variable_set "@#{name}", fixture.find
483           rescue FixtureClassNotFound
484             nil
485           end
486         end
487       end
488     end
489   end
490
491   def self.instantiate_all_loaded_fixtures(object, load_instances = true)
492     all_loaded_fixtures.each do |table_name, fixtures|
493       Fixtures.instantiate_fixtures(object, table_name, fixtures, load_instances)
494     end
495   end
496
497   cattr_accessor :all_loaded_fixtures
498   self.all_loaded_fixtures = {}
499
500   def self.create_fixtures(fixtures_directory, table_names, class_names = {})
501     table_names = [table_names].flatten.map { |n| n.to_s }
502     connection  = block_given? ? yield : ActiveRecord::Base.connection
503
504     table_names_to_fetch = table_names.reject { |table_name| fixture_is_cached?(connection, table_name) }
505
506     unless table_names_to_fetch.empty?
507       ActiveRecord::Base.silence do
508         connection.disable_referential_integrity do
509           fixtures_map = {}
510
511           fixtures = table_names_to_fetch.map do |table_name|
512             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))
513           end
514
515           all_loaded_fixtures.update(fixtures_map)
516
517           connection.transaction(Thread.current['open_transactions'].to_i == 0) do
518             fixtures.reverse.each { |fixture| fixture.delete_existing_fixtures }
519             fixtures.each { |fixture| fixture.insert_fixtures }
520
521             # Cap primary key sequences to max(pk).
522             if connection.respond_to?(:reset_pk_sequence!)
523               table_names.each do |table_name|
524                 connection.reset_pk_sequence!(table_name)
525               end
526             end
527           end
528
529           cache_fixtures(connection, fixtures)
530         end
531       end
532     end
533     cached_fixtures(connection, table_names)
534   end
535
536   # Returns a consistent identifier for +label+. This will always
537   # be a positive integer, and will always be the same for a given
538   # label, assuming the same OS, platform, and version of Ruby.
539   def self.identify(label)
540     label.to_s.hash.abs
541   end
542
543   attr_reader :table_name
544
545   def initialize(connection, table_name, class_name, fixture_path, file_filter = DEFAULT_FILTER_RE)
546     @connection, @table_name, @fixture_path, @file_filter = connection, table_name, fixture_path, file_filter
547     @class_name = class_name ||
548                   (ActiveRecord::Base.pluralize_table_names ? @table_name.singularize.camelize : @table_name.camelize)
549     @table_name = ActiveRecord::Base.table_name_prefix + @table_name + ActiveRecord::Base.table_name_suffix
550     @table_name = class_name.table_name if class_name.respond_to?(:table_name)
551     @connection = class_name.connection if class_name.respond_to?(:connection)
552     read_fixture_files
553   end
554
555   def delete_existing_fixtures
556     @connection.delete "DELETE FROM #{@connection.quote_table_name(table_name)}", 'Fixture Delete'
557   end
558
559   def insert_fixtures
560     now = ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now
561     now = now.to_s(:db)
562
563     # allow a standard key to be used for doing defaults in YAML
564     if is_a?(Hash)
565       delete('DEFAULTS')
566     else
567       delete(assoc('DEFAULTS'))
568     end
569
570     # track any join tables we need to insert later
571     habtm_fixtures = Hash.new do |h, habtm|
572       h[habtm] = HabtmFixtures.new(@connection, habtm.options[:join_table], nil, nil)
573     end
574
575     each do |label, fixture|
576       row = fixture.to_hash
577
578       if model_class && model_class < ActiveRecord::Base
579         # fill in timestamp columns if they aren't specified and the model is set to record_timestamps
580         if model_class.record_timestamps
581           timestamp_column_names.each do |name|
582             row[name] = now unless row.key?(name)
583           end
584         end
585
586         # interpolate the fixture label
587         row.each do |key, value|
588           row[key] = label if value == "$LABEL"
589         end
590
591         # generate a primary key if necessary
592         if has_primary_key_column? && !row.include?(primary_key_name)
593           row[primary_key_name] = Fixtures.identify(label)
594         end
595
596         # If STI is used, find the correct subclass for association reflection
597         reflection_class =
598           if row.include?(inheritance_column_name)
599             row[inheritance_column_name].constantize rescue model_class
600           else
601             model_class
602           end
603
604         reflection_class.reflect_on_all_associations.each do |association|
605           case association.macro
606           when :belongs_to
607             # Do not replace association name with association foreign key if they are named the same
608             fk_name = (association.options[:foreign_key] || "#{association.name}_id").to_s
609
610             if association.name.to_s != fk_name && value = row.delete(association.name.to_s)
611               if association.options[:polymorphic]
612                 if value.sub!(/\s*\(([^\)]*)\)\s*$/, "")
613                   target_type = $1
614                   target_type_name = (association.options[:foreign_type] || "#{association.name}_type").to_s
615
616                   # support polymorphic belongs_to as "label (Type)"
617                   row[target_type_name] = target_type
618                 end
619               end
620
621               row[fk_name] = Fixtures.identify(value)
622             end
623           when :has_and_belongs_to_many
624             if (targets = row.delete(association.name.to_s))
625               targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/)
626               join_fixtures = habtm_fixtures[association]
627
628               targets.each do |target|
629                 join_fixtures["#{label}_#{target}"] = Fixture.new(
630                   { association.primary_key_name => row[primary_key_name],
631                     association.association_foreign_key => Fixtures.identify(target) }, nil)
632               end
633             end
634           end
635         end
636       end
637
638       @connection.insert_fixture(fixture, @table_name)
639     end
640
641     # insert any HABTM join tables we discovered
642     habtm_fixtures.values.each do |fixture|
643       fixture.delete_existing_fixtures
644       fixture.insert_fixtures
645     end
646   end
647
648   private
649     class HabtmFixtures < ::Fixtures #:nodoc:
650       def read_fixture_files; end
651     end
652
653     def model_class
654       unless defined?(@model_class)
655         @model_class =
656           if @class_name.nil? || @class_name.is_a?(Class)
657             @class_name
658           else
659             @class_name.constantize rescue nil
660           end
661       end
662
663       @model_class
664     end
665
666     def primary_key_name
667       @primary_key_name ||= model_class && model_class.primary_key
668     end
669
670     def has_primary_key_column?
671       @has_primary_key_column ||= model_class && primary_key_name &&
672         model_class.columns.find { |c| c.name == primary_key_name }
673     end
674
675     def timestamp_column_names
676       @timestamp_column_names ||= %w(created_at created_on updated_at updated_on).select do |name|
677         column_names.include?(name)
678       end
679     end
680
681     def inheritance_column_name
682       @inheritance_column_name ||= model_class && model_class.inheritance_column
683     end
684
685     def column_names
686       @column_names ||= @connection.columns(@table_name).collect(&:name)
687     end
688
689     def read_fixture_files
690       if File.file?(yaml_file_path)
691         read_yaml_fixture_files
692       elsif File.file?(csv_file_path)
693         read_csv_fixture_files
694       end
695     end
696
697     def read_yaml_fixture_files
698       yaml_string = ""
699       Dir["#{@fixture_path}/**/*.yml"].select { |f| test(?f, f) }.each do |subfixture_path|
700         yaml_string << IO.read(subfixture_path)
701       end
702       yaml_string << IO.read(yaml_file_path)
703
704       if yaml = parse_yaml_string(yaml_string)
705         # If the file is an ordered map, extract its children.
706         yaml_value =
707           if yaml.respond_to?(:type_id) && yaml.respond_to?(:value)
708             yaml.value
709           else
710             [yaml]
711           end
712
713         yaml_value.each do |fixture|
714           raise Fixture::FormatError, "Bad data for #{@class_name} fixture named #{fixture}" unless fixture.respond_to?(:each)
715           fixture.each do |name, data|
716             unless data
717               raise Fixture::FormatError, "Bad data for #{@class_name} fixture named #{name} (nil)"
718             end
719
720             self[name] = Fixture.new(data, model_class)
721           end
722         end
723       end
724     end
725
726     def read_csv_fixture_files
727       reader = CSV.parse(erb_render(IO.read(csv_file_path)))
728       header = reader.shift
729       i = 0
730       reader.each do |row|
731         data = {}
732         row.each_with_index { |cell, j| data[header[j].to_s.strip] = cell.to_s.strip }
733         self["#{Inflector::underscore(@class_name)}_#{i+=1}"] = Fixture.new(data, model_class)
734       end
735     end
736
737     def yaml_file_path
738       "#{@fixture_path}.yml"
739     end
740
741     def csv_file_path
742       @fixture_path + ".csv"
743     end
744
745     def yaml_fixtures_key(path)
746       File.basename(@fixture_path).split(".").first
747     end
748
749     def parse_yaml_string(fixture_content)
750       YAML::load(erb_render(fixture_content))
751     rescue => error
752       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}"
753     end
754
755     def erb_render(fixture_content)
756       ERB.new(fixture_content).result
757     end
758 end
759
760 class Fixture #:nodoc:
761   include Enumerable
762
763   class FixtureError < StandardError #:nodoc:
764   end
765
766   class FormatError < FixtureError #:nodoc:
767   end
768
769   attr_reader :model_class
770
771   def initialize(fixture, model_class)
772     @fixture = fixture
773     @model_class = model_class.is_a?(Class) ? model_class : model_class.constantize rescue nil
774   end
775
776   def class_name
777     @model_class.name if @model_class
778   end
779
780   def each
781     @fixture.each { |item| yield item }
782   end
783
784   def [](key)
785     @fixture[key]
786   end
787
788   def to_hash
789     @fixture
790   end
791
792   def key_list
793     columns = @fixture.keys.collect{ |column_name| ActiveRecord::Base.connection.quote_column_name(column_name) }
794     columns.join(", ")
795   end
796
797   def value_list
798     list = @fixture.inject([]) do |fixtures, (key, value)|
799       col = model_class.columns_hash[key] if model_class.respond_to?(:ancestors) && model_class.ancestors.include?(ActiveRecord::Base)
800       fixtures << ActiveRecord::Base.connection.quote(value, col).gsub('[^\]\\n', "\n").gsub('[^\]\\r', "\r")
801     end
802     list * ', '
803   end
804
805   def find
806     if model_class
807       model_class.find(self[model_class.primary_key])
808     else
809       raise FixtureClassNotFound, "No class attached to find."
810     end
811   end
812 end
813
814 module Test #:nodoc:
815   module Unit #:nodoc:
816     class TestCase #:nodoc:
817       setup :setup_fixtures
818       teardown :teardown_fixtures
819
820       superclass_delegating_accessor :fixture_path
821       superclass_delegating_accessor :fixture_table_names
822       superclass_delegating_accessor :fixture_class_names
823       superclass_delegating_accessor :use_transactional_fixtures
824       superclass_delegating_accessor :use_instantiated_fixtures   # true, false, or :no_instances
825       superclass_delegating_accessor :pre_loaded_fixtures
826
827       self.fixture_table_names = []
828       self.use_transactional_fixtures = false
829       self.use_instantiated_fixtures = true
830       self.pre_loaded_fixtures = false
831
832       @@already_loaded_fixtures = {}
833       self.fixture_class_names = {}
834
835       class << self
836         def set_fixture_class(class_names = {})
837           self.fixture_class_names = self.fixture_class_names.merge(class_names)
838         end
839
840         def fixtures(*table_names)
841           if table_names.first == :all
842             table_names = Dir["#{fixture_path}/*.yml"] + Dir["#{fixture_path}/*.csv"]
843             table_names.map! { |f| File.basename(f).split('.')[0..-2].join('.') }
844           else
845             table_names = table_names.flatten.map { |n| n.to_s }
846           end
847
848           self.fixture_table_names |= table_names
849           require_fixture_classes(table_names)
850           setup_fixture_accessors(table_names)
851         end
852
853         def try_to_load_dependency(file_name)
854           require_dependency file_name
855         rescue LoadError => e
856           # Let's hope the developer has included it himself
857          
858           # Let's warn in case this is a subdependency, otherwise
859           # subdependency error messages are totally cryptic
860           if ActiveRecord::Base.logger
861             ActiveRecord::Base.logger.warn("Unable to load #{file_name}, underlying cause #{e.message} \n\n #{e.backtrace.join("\n")}")
862           end
863         end
864        
865         def require_fixture_classes(table_names = nil)
866           (table_names || fixture_table_names).each do |table_name|
867             file_name = table_name.to_s
868             file_name = file_name.singularize if ActiveRecord::Base.pluralize_table_names
869             try_to_load_dependency(file_name)
870           end
871         end
872
873         def setup_fixture_accessors(table_names = nil)
874           table_names = [table_names] if table_names && !table_names.respond_to?(:each)
875           (table_names || fixture_table_names).each do |table_name|
876             table_name = table_name.to_s.tr('.', '_')
877
878             define_method(table_name) do |*fixtures|
879               force_reload = fixtures.pop if fixtures.last == true || fixtures.last == :reload
880
881               @fixture_cache[table_name] ||= {}
882
883               instances = fixtures.map do |fixture|
884                 @fixture_cache[table_name].delete(fixture) if force_reload
885
886                 if @loaded_fixtures[table_name][fixture.to_s]
887                   @fixture_cache[table_name][fixture] ||= @loaded_fixtures[table_name][fixture.to_s].find
888                 else
889                   raise StandardError, "No fixture with name '#{fixture}' found for table '#{table_name}'"
890                 end
891               end
892
893               instances.size == 1 ? instances.first : instances
894             end
895           end
896         end
897
898         def uses_transaction(*methods)
899           @uses_transaction = [] unless defined?(@uses_transaction)
900           @uses_transaction.concat methods.map(&:to_s)
901         end
902
903         def uses_transaction?(method)
904           @uses_transaction = [] unless defined?(@uses_transaction)
905           @uses_transaction.include?(method.to_s)
906         end
907       end
908
909       def use_transactional_fixtures?
910         use_transactional_fixtures &&
911           !self.class.uses_transaction?(method_name)
912       end
913
914       def setup_fixtures
915         return unless defined?(ActiveRecord) && !ActiveRecord::Base.configurations.blank?
916
917         if pre_loaded_fixtures && !use_transactional_fixtures
918           raise RuntimeError, 'pre_loaded_fixtures requires use_transactional_fixtures'
919         end
920
921         @fixture_cache = {}
922
923         # Load fixtures once and begin transaction.
924         if use_transactional_fixtures?
925           if @@already_loaded_fixtures[self.class]
926             @loaded_fixtures = @@already_loaded_fixtures[self.class]
927           else
928             load_fixtures
929             @@already_loaded_fixtures[self.class] = @loaded_fixtures
930           end
931           ActiveRecord::Base.send :increment_open_transactions
932           ActiveRecord::Base.connection.begin_db_transaction
933         # Load fixtures for every test.
934         else
935           Fixtures.reset_cache
936           @@already_loaded_fixtures[self.class] = nil
937           load_fixtures
938         end
939
940         # Instantiate fixtures for every test if requested.
941         instantiate_fixtures if use_instantiated_fixtures
942       end
943
944       def teardown_fixtures
945         return unless defined?(ActiveRecord) && !ActiveRecord::Base.configurations.blank?
946
947         unless use_transactional_fixtures?
948           Fixtures.reset_cache
949         end
950
951         # Rollback changes if a transaction is active.
952         if use_transactional_fixtures? && Thread.current['open_transactions'] != 0
953           ActiveRecord::Base.connection.rollback_db_transaction
954           Thread.current['open_transactions'] = 0
955         end
956         ActiveRecord::Base.verify_active_connections!
957       end
958
959       private
960         def load_fixtures
961           @loaded_fixtures = {}
962           fixtures = Fixtures.create_fixtures(fixture_path, fixture_table_names, fixture_class_names)
963           unless fixtures.nil?
964             if fixtures.instance_of?(Fixtures)
965               @loaded_fixtures[fixtures.table_name] = fixtures
966             else
967               fixtures.each { |f| @loaded_fixtures[f.table_name] = f }
968             end
969           end
970         end
971
972         # for pre_loaded_fixtures, only require the classes once. huge speed improvement
973         @@required_fixture_classes = false
974
975         def instantiate_fixtures
976           if pre_loaded_fixtures
977             raise RuntimeError, 'Load fixtures before instantiating them.' if Fixtures.all_loaded_fixtures.empty?
978             unless @@required_fixture_classes
979               self.class.require_fixture_classes Fixtures.all_loaded_fixtures.keys
980               @@required_fixture_classes = true
981             end
982             Fixtures.instantiate_all_loaded_fixtures(self, load_instances?)
983           else
984             raise RuntimeError, 'Load fixtures before instantiating them.' if @loaded_fixtures.nil?
985             @loaded_fixtures.each do |table_name, fixtures|
986               Fixtures.instantiate_fixtures(self, table_name, fixtures, load_instances?)
987             end
988           end
989         end
990
991         def load_instances?
992           use_instantiated_fixtures != :no_instances
993         end
994     end
995   end
996 end
Note: See TracBrowser for help on using the browser.