dm-fixtures 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,998 @@
1
+ require 'erb'
2
+
3
+ begin
4
+ require 'psych'
5
+ rescue LoadError
6
+ end
7
+
8
+ require 'yaml'
9
+ require 'zlib'
10
+ require 'active_support/dependencies'
11
+ require 'active_support/core_ext/array/wrap'
12
+ require 'active_support/core_ext/object/blank'
13
+ require 'active_support/core_ext/logger'
14
+ require 'active_support/ordered_hash'
15
+ require 'dm-fixtures/fixture_set/file'
16
+
17
+ if defined? ActiveRecord
18
+ class FixtureClassNotFound < ActiveRecord::ActiveRecordError #:nodoc:
19
+ end
20
+ else
21
+ class FixtureClassNotFound < StandardError #:nodoc:
22
+ end
23
+ end
24
+
25
+ module DataMapper
26
+ # \Fixtures are a way of organizing data that you want to test against; in short, sample data.
27
+ #
28
+ # They are stored in YAML files, one file per model, which are placed in the directory
29
+ # appointed by <tt>ActiveSupport::TestCase.fixture_path=(path)</tt> (this is automatically
30
+ # configured for Rails, so you can just put your files in <tt><your-rails-app>/test/fixtures/</tt>).
31
+ # The fixture file ends with the <tt>.yml</tt> file extension (Rails example:
32
+ # <tt><your-rails-app>/test/fixtures/web_sites.yml</tt>). The format of a fixture file looks
33
+ # like this:
34
+ #
35
+ # rubyonrails:
36
+ # id: 1
37
+ # name: Ruby on Rails
38
+ # url: http://www.rubyonrails.org
39
+ #
40
+ # google:
41
+ # id: 2
42
+ # name: Google
43
+ # url: http://www.google.com
44
+ #
45
+ # This fixture file includes two fixtures. Each YAML fixture (ie. record) is given a name and
46
+ # is followed by an indented list of key/value pairs in the "key: value" format. Records are
47
+ # separated by a blank line for your viewing pleasure.
48
+ #
49
+ # Note that fixtures are unordered. If you want ordered fixtures, use the omap YAML type.
50
+ # See http://yaml.org/type/omap.html
51
+ # for the specification. You will need ordered fixtures when you have foreign key constraints
52
+ # on keys in the same table. This is commonly needed for tree structures. Example:
53
+ #
54
+ # --- !omap
55
+ # - parent:
56
+ # id: 1
57
+ # parent_id: NULL
58
+ # title: Parent
59
+ # - child:
60
+ # id: 2
61
+ # parent_id: 1
62
+ # title: Child
63
+ #
64
+ # = Using Fixtures in Test Cases
65
+ #
66
+ # Since fixtures are a testing construct, we use them in our unit and functional tests. There
67
+ # are two ways to use the fixtures, but first let's take a look at a sample unit test:
68
+ #
69
+ # require 'test_helper'
70
+ #
71
+ # class WebSiteTest < ActiveSupport::TestCase
72
+ # test "web_site_count" do
73
+ # assert_equal 2, WebSite.count
74
+ # end
75
+ # end
76
+ #
77
+ # By default, <tt>test_helper.rb</tt> will load all of your fixtures into your test database,
78
+ # so this test will succeed.
79
+ #
80
+ # The testing environment will automatically load the all fixtures into the database before each
81
+ # test. To ensure consistent data, the environment deletes the fixtures before running the load.
82
+ #
83
+ # In addition to being available in the database, the fixture's data may also be accessed by
84
+ # using a special dynamic method, which has the same name as the model, and accepts the
85
+ # name of the fixture to instantiate:
86
+ #
87
+ # test "find" do
88
+ # assert_equal "Ruby on Rails", web_sites(:rubyonrails).name
89
+ # end
90
+ #
91
+ # Alternatively, you may enable auto-instantiation of the fixture data. For instance, take the
92
+ # following tests:
93
+ #
94
+ # test "find_alt_method_1" do
95
+ # assert_equal "Ruby on Rails", @web_sites['rubyonrails']['name']
96
+ # end
97
+ #
98
+ # test "find_alt_method_2" do
99
+ # assert_equal "Ruby on Rails", @rubyonrails.news
100
+ # end
101
+ #
102
+ # In order to use these methods to access fixtured data within your testcases, you must specify one of the
103
+ # following in your <tt>ActiveSupport::TestCase</tt>-derived class:
104
+ #
105
+ # - to fully enable instantiated fixtures (enable alternate methods #1 and #2 above)
106
+ # self.use_instantiated_fixtures = true
107
+ #
108
+ # - create only the hash for the fixtures, do not 'find' each instance (enable alternate method #1 only)
109
+ # self.use_instantiated_fixtures = :no_instances
110
+ #
111
+ # Using either of these alternate methods incurs a performance hit, as the fixtured data must be fully
112
+ # traversed in the database to create the fixture hash and/or instance variables. This is expensive for
113
+ # large sets of fixtured data.
114
+ #
115
+ # = Dynamic fixtures with ERB
116
+ #
117
+ # Some times you don't care about the content of the fixtures as much as you care about the volume.
118
+ # In these cases, you can mix ERB in with your YAML fixtures to create a bunch of fixtures for load
119
+ # testing, like:
120
+ #
121
+ # <% 1.upto(1000) do |i| %>
122
+ # fix_<%= i %>:
123
+ # id: <%= i %>
124
+ # name: guy_<%= 1 %>
125
+ # <% end %>
126
+ #
127
+ # This will create 1000 very simple fixtures.
128
+ #
129
+ # Using ERB, you can also inject dynamic values into your fixtures with inserts like
130
+ # <tt><%= Date.today.strftime("%Y-%m-%d") %></tt>.
131
+ # This is however a feature to be used with some caution. The point of fixtures are that they're
132
+ # stable units of predictable sample data. If you feel that you need to inject dynamic values, then
133
+ # perhaps you should reexamine whether your application is properly testable. Hence, dynamic values
134
+ # in fixtures are to be considered a code smell.
135
+ #
136
+ # = Transactional Fixtures
137
+ #
138
+ # Test cases can use begin+rollback to isolate their changes to the database instead of having to
139
+ # delete+insert for every test case.
140
+ #
141
+ # class FooTest < ActiveSupport::TestCase
142
+ # self.use_transactional_fixtures = true
143
+ #
144
+ # test "godzilla" do
145
+ # assert !Foo.all.empty?
146
+ # Foo.destroy_all
147
+ # assert Foo.all.empty?
148
+ # end
149
+ #
150
+ # test "godzilla aftermath" do
151
+ # assert !Foo.all.empty?
152
+ # end
153
+ # end
154
+ #
155
+ # If you preload your test database with all fixture data (probably in the rake task) and use
156
+ # transactional fixtures, then you may omit all fixtures declarations in your test cases since
157
+ # all the data's already there and every case rolls back its changes.
158
+ #
159
+ # In order to use instantiated fixtures with preloaded data, set +self.pre_loaded_fixtures+ to
160
+ # true. This will provide access to fixture data for every table that has been loaded through
161
+ # fixtures (depending on the value of +use_instantiated_fixtures+).
162
+ #
163
+ # When *not* to use transactional fixtures:
164
+ #
165
+ # 1. You're testing whether a transaction works correctly. Nested transactions don't commit until
166
+ # all parent transactions commit, particularly, the fixtures transaction which is begun in setup
167
+ # and rolled back in teardown. Thus, you won't be able to verify
168
+ # the results of your transaction until Active Record supports nested transactions or savepoints (in progress).
169
+ # 2. Your database does not support transactions. Every Active Record database supports transactions except MySQL MyISAM.
170
+ # Use InnoDB, MaxDB, or NDB instead.
171
+ #
172
+ # = Advanced Fixtures
173
+ #
174
+ # Fixtures that don't specify an ID get some extra features:
175
+ #
176
+ # * Stable, autogenerated IDs
177
+ # * Label references for associations (belongs_to, has_one, has_many)
178
+ # * HABTM associations as inline lists
179
+ # * Autofilled timestamp columns
180
+ # * Fixture label interpolation
181
+ # * Support for YAML defaults
182
+ #
183
+ # == Stable, Autogenerated IDs
184
+ #
185
+ # Here, have a monkey fixture:
186
+ #
187
+ # george:
188
+ # id: 1
189
+ # name: George the Monkey
190
+ #
191
+ # reginald:
192
+ # id: 2
193
+ # name: Reginald the Pirate
194
+ #
195
+ # Each of these fixtures has two unique identifiers: one for the database
196
+ # and one for the humans. Why don't we generate the primary key instead?
197
+ # Hashing each fixture's label yields a consistent ID:
198
+ #
199
+ # george: # generated id: 503576764
200
+ # name: George the Monkey
201
+ #
202
+ # reginald: # generated id: 324201669
203
+ # name: Reginald the Pirate
204
+ #
205
+ # Active Record looks at the fixture's model class, discovers the correct
206
+ # primary key, and generates it right before inserting the fixture
207
+ # into the database.
208
+ #
209
+ # The generated ID for a given label is constant, so we can discover
210
+ # any fixture's ID without loading anything, as long as we know the label.
211
+ #
212
+ # == Label references for associations (belongs_to, has_one, has_many)
213
+ #
214
+ # Specifying foreign keys in fixtures can be very fragile, not to
215
+ # mention difficult to read. Since Active Record can figure out the ID of
216
+ # any fixture from its label, you can specify FK's by label instead of ID.
217
+ #
218
+ # === belongs_to
219
+ #
220
+ # Let's break out some more monkeys and pirates.
221
+ #
222
+ # ### in pirates.yml
223
+ #
224
+ # reginald:
225
+ # id: 1
226
+ # name: Reginald the Pirate
227
+ # monkey_id: 1
228
+ #
229
+ # ### in monkeys.yml
230
+ #
231
+ # george:
232
+ # id: 1
233
+ # name: George the Monkey
234
+ # pirate_id: 1
235
+ #
236
+ # Add a few more monkeys and pirates and break this into multiple files,
237
+ # and it gets pretty hard to keep track of what's going on. Let's
238
+ # use labels instead of IDs:
239
+ #
240
+ # ### in pirates.yml
241
+ #
242
+ # reginald:
243
+ # name: Reginald the Pirate
244
+ # monkey: george
245
+ #
246
+ # ### in monkeys.yml
247
+ #
248
+ # george:
249
+ # name: George the Monkey
250
+ # pirate: reginald
251
+ #
252
+ # Pow! All is made clear. Active Record reflects on the fixture's model class,
253
+ # finds all the +belongs_to+ associations, and allows you to specify
254
+ # a target *label* for the *association* (monkey: george) rather than
255
+ # a target *id* for the *FK* (<tt>monkey_id: 1</tt>).
256
+ #
257
+ # ==== Polymorphic belongs_to
258
+ #
259
+ # Supporting polymorphic relationships is a little bit more complicated, since
260
+ # Active Record needs to know what type your association is pointing at. Something
261
+ # like this should look familiar:
262
+ #
263
+ # ### in fruit.rb
264
+ #
265
+ # belongs_to :eater, :polymorphic => true
266
+ #
267
+ # ### in fruits.yml
268
+ #
269
+ # apple:
270
+ # id: 1
271
+ # name: apple
272
+ # eater_id: 1
273
+ # eater_type: Monkey
274
+ #
275
+ # Can we do better? You bet!
276
+ #
277
+ # apple:
278
+ # eater: george (Monkey)
279
+ #
280
+ # Just provide the polymorphic target type and Active Record will take care of the rest.
281
+ #
282
+ # === has_and_belongs_to_many
283
+ #
284
+ # Time to give our monkey some fruit.
285
+ #
286
+ # ### in monkeys.yml
287
+ #
288
+ # george:
289
+ # id: 1
290
+ # name: George the Monkey
291
+ #
292
+ # ### in fruits.yml
293
+ #
294
+ # apple:
295
+ # id: 1
296
+ # name: apple
297
+ #
298
+ # orange:
299
+ # id: 2
300
+ # name: orange
301
+ #
302
+ # grape:
303
+ # id: 3
304
+ # name: grape
305
+ #
306
+ # ### in fruits_monkeys.yml
307
+ #
308
+ # apple_george:
309
+ # fruit_id: 1
310
+ # monkey_id: 1
311
+ #
312
+ # orange_george:
313
+ # fruit_id: 2
314
+ # monkey_id: 1
315
+ #
316
+ # grape_george:
317
+ # fruit_id: 3
318
+ # monkey_id: 1
319
+ #
320
+ # Let's make the HABTM fixture go away.
321
+ #
322
+ # ### in monkeys.yml
323
+ #
324
+ # george:
325
+ # id: 1
326
+ # name: George the Monkey
327
+ # fruits: apple, orange, grape
328
+ #
329
+ # ### in fruits.yml
330
+ #
331
+ # apple:
332
+ # name: apple
333
+ #
334
+ # orange:
335
+ # name: orange
336
+ #
337
+ # grape:
338
+ # name: grape
339
+ #
340
+ # Zap! No more fruits_monkeys.yml file. We've specified the list of fruits
341
+ # on George's fixture, but we could've just as easily specified a list
342
+ # of monkeys on each fruit. As with +belongs_to+, Active Record reflects on
343
+ # the fixture's model class and discovers the +has_and_belongs_to_many+
344
+ # associations.
345
+ #
346
+ # == Autofilled Timestamp Columns
347
+ #
348
+ # If your table/model specifies any of Active Record's
349
+ # standard timestamp columns (+created_at+, +created_on+, +updated_at+, +updated_on+),
350
+ # they will automatically be set to <tt>Time.now</tt>.
351
+ #
352
+ # If you've set specific values, they'll be left alone.
353
+ #
354
+ # == Fixture label interpolation
355
+ #
356
+ # The label of the current fixture is always available as a column value:
357
+ #
358
+ # geeksomnia:
359
+ # name: Geeksomnia's Account
360
+ # subdomain: $LABEL
361
+ #
362
+ # Also, sometimes (like when porting older join table fixtures) you'll need
363
+ # to be able to get a hold of the identifier for a given label. ERB
364
+ # to the rescue:
365
+ #
366
+ # george_reginald:
367
+ # monkey_id: <%= DataMapper::Fixtures.identify(:reginald) %>
368
+ # pirate_id: <%= DataMapper::Fixtures.identify(:george) %>
369
+ #
370
+ # == Support for YAML defaults
371
+ #
372
+ # You probably already know how to use YAML to set and reuse defaults in
373
+ # your <tt>database.yml</tt> file. You can use the same technique in your fixtures:
374
+ #
375
+ # DEFAULTS: &DEFAULTS
376
+ # created_on: <%= 3.weeks.ago.to_s(:db) %>
377
+ #
378
+ # first:
379
+ # name: Smurf
380
+ # *DEFAULTS
381
+ #
382
+ # second:
383
+ # name: Fraggle
384
+ # *DEFAULTS
385
+ #
386
+ # Any fixture labeled "DEFAULTS" is safely ignored.
387
+ class Fixtures
388
+ MAX_ID = 2 ** 30 - 1
389
+
390
+ @@all_cached_fixtures = Hash.new { |h,k| h[k] = {} }
391
+
392
+ def self.find_table_name(table_name) # :nodoc:
393
+ convention = DataMapper.repository.adapter.resource_naming_convention #TODO do we want default or something like test?
394
+ # DataMapper::NamingConventions::Resource::Underscored
395
+ # DataMapper::NamingConventions::Resource::UnderscoredAndPluralized
396
+ # DataMapper::NamingConventions::Resource::UnderscoredAndPluralizedWithoutModule
397
+ convention != DataMapper::NamingConventions::Resource::Underscored ?
398
+ table_name.singularize.camelize :
399
+ table_name.camelize
400
+ end
401
+
402
+ def self.reset_cache
403
+ @@all_cached_fixtures.clear
404
+ end
405
+
406
+ def self.cache_for_connection(connection)
407
+ @@all_cached_fixtures[connection]
408
+ end
409
+
410
+ def self.fixture_is_cached?(connection, table_name)
411
+ cache_for_connection(connection)[table_name]
412
+ end
413
+
414
+ def self.cached_fixtures(connection, keys_to_fetch = nil)
415
+ if keys_to_fetch
416
+ cache_for_connection(connection).values_at(*keys_to_fetch)
417
+ else
418
+ cache_for_connection(connection).values
419
+ end
420
+ end
421
+
422
+ def self.cache_fixtures(connection, fixtures_map)
423
+ cache_for_connection(connection).update(fixtures_map)
424
+ end
425
+
426
+ #--
427
+ # TODO:NOTE: in the next version, the __with_new_arity suffix and
428
+ # the method with the old arity will be removed.
429
+ #++
430
+ def self.instantiate_fixtures__with_new_arity(object, fixture_set, load_instances = true) # :nodoc:
431
+ if load_instances
432
+ fixture_set.each do |fixture_name, fixture|
433
+ begin
434
+ object.instance_variable_set "@#{fixture_name}", fixture.find
435
+ rescue FixtureClassNotFound
436
+ nil
437
+ end
438
+ end
439
+ end
440
+ end
441
+
442
+ # The use with parameters <tt>(object, fixture_set_name, fixture_set, load_instances = true)</tt> is deprecated, +fixture_set_name+ parameter is not used.
443
+ # Use as:
444
+ #
445
+ # instantiate_fixtures(object, fixture_set, load_instances = true)
446
+ def self.instantiate_fixtures(object, fixture_set, load_instances = true, rails_3_2_compatibility_argument = true)
447
+ unless load_instances == true || load_instances == false
448
+ ActiveSupport::Deprecation.warn(
449
+ "ActiveRecord::Fixtures.instantiate_fixtures with parameters (object, fixture_set_name, fixture_set, load_instances = true) is deprecated and shall be removed from future releases. Use it with parameters (object, fixture_set, load_instances = true) instead (skip fixture_set_name).",
450
+ caller)
451
+ fixture_set = load_instances
452
+ load_instances = rails_3_2_compatibility_argument
453
+ end
454
+ instantiate_fixtures__with_new_arity(object, fixture_set, load_instances)
455
+ end
456
+
457
+ def self.instantiate_all_loaded_fixtures(object, load_instances = true)
458
+ all_loaded_fixtures.each_value do |fixture_set|
459
+ DataMapper::Fixtures.instantiate_fixtures(object, fixture_set, load_instances)
460
+ end
461
+ end
462
+
463
+ cattr_accessor :all_loaded_fixtures
464
+ self.all_loaded_fixtures = {}
465
+
466
+ def self.create_fixtures(fixtures_directory, table_names, class_names = {})
467
+ table_names = [table_names].flatten.map { |n| n.to_s }
468
+ table_names.each { |n|
469
+ class_names[n.tr('/', '_').to_sym] = n.classify if n.include?('/')
470
+ }
471
+
472
+ connection = block_given? ? yield : DataMapper.repository #ActiveRecord::Base.connection
473
+
474
+ files_to_read = table_names.reject { |table_name|
475
+ fixture_is_cached?(connection, table_name)
476
+ }
477
+
478
+ unless files_to_read.empty?
479
+ connection.adapter.disable_referential_integrity do
480
+ fixtures_map = {}
481
+
482
+ fixture_files = files_to_read.map do |path|
483
+ table_name = path.tr '/', '_'
484
+
485
+ fixtures_map[path] = DataMapper::Fixtures.new(
486
+ connection,
487
+ table_name,
488
+ !class_names.empty? ? class_names[table_name.to_sym] : table_name.classify,
489
+ ::File.join(fixtures_directory, path)
490
+ )
491
+ end
492
+
493
+ all_loaded_fixtures.update(fixtures_map)
494
+
495
+ # Implicitly already in transaction if necessary
496
+ #connection.transaction do #(:requires_new => true)
497
+ fixture_files.each do |ff|
498
+ conn = ff.model_class.respond_to?(:current_connection) ? ff.model_class.current_connection : connection
499
+ table_rows = ff.table_rows
500
+
501
+ table_rows.keys.each do |table|
502
+ # Delete all fixtures from table using their id
503
+ #conn.delete "DELETE FROM #{conn.quote_table_name(table)}", 'Fixture Delete'
504
+ success = ff.model_class.destroy!
505
+ end
506
+
507
+ table_rows.each do |table_name,rows|
508
+ rows.each do |row|
509
+ #conn.insert_fixture(row, table_name) #TODO
510
+ ff.model_class.create!(row.symbolize_keys)
511
+ end
512
+ end
513
+ end
514
+
515
+ # Cap primary key sequences to max(pk).
516
+ if connection.respond_to?(:reset_pk_sequence!)
517
+ table_names.each do |table_name|
518
+ connection.reset_pk_sequence!(table_name.tr('/', '_'))
519
+ end
520
+ end
521
+ #end
522
+
523
+ cache_fixtures(connection, fixtures_map)
524
+ end
525
+ end
526
+ cached_fixtures(connection, table_names)
527
+ end
528
+
529
+ # Returns a consistent, platform-independent identifier for +label+.
530
+ # Identifiers are positive integers less than 2^32.
531
+ def self.identify(label)
532
+ Zlib.crc32(label.to_s) % MAX_ID
533
+ end
534
+
535
+ attr_reader :table_name, :name, :fixtures, :model_class
536
+
537
+ def initialize(connection, table_name, class_name, fixture_path)
538
+ @connection = connection # instance of DataMapper::Repository (usually DataMapper.repository)
539
+ @table_name = table_name
540
+ @fixture_path = fixture_path
541
+ @name = table_name # preserve fixture base name
542
+ @class_name = class_name
543
+
544
+ @fixtures = ActiveSupport::OrderedHash.new
545
+ @table_name = DataMapper.repository.adapter.resource_naming_convention.call(@table_name)
546
+
547
+ # Should be a class that: `include DataMapper::Resource`
548
+ if class_name.is_a?(Class)
549
+ @table_name = class_name.table_name
550
+ @connection = class_name.connection
551
+ @model_class = class_name
552
+ else
553
+ @model_class = class_name.constantize rescue nil
554
+ end
555
+
556
+ read_fixture_files
557
+ end
558
+
559
+ def [](x)
560
+ fixtures[x]
561
+ end
562
+
563
+ def []=(k,v)
564
+ fixtures[k] = v
565
+ end
566
+
567
+ def each(&block)
568
+ fixtures.each(&block)
569
+ end
570
+
571
+ def size
572
+ fixtures.size
573
+ end
574
+
575
+ # Return a hash of rows to be inserted. The key is the table, the value is
576
+ # a list of rows to insert to that table.
577
+ def table_rows
578
+ now = Time.now #ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now
579
+ now = now.to_s(:db)
580
+
581
+ # allow a standard key to be used for doing defaults in YAML
582
+ fixtures.delete('DEFAULTS')
583
+
584
+ # track any join tables we need to insert later
585
+ rows = Hash.new { |h,table| h[table] = [] }
586
+
587
+ rows[table_name] = fixtures.map do |label, fixture|
588
+ row = fixture.to_hash
589
+
590
+ if model_class && DataMapper::Model.descendants.entries.include?(model_class) #model_class < ActiveRecord::Base
591
+ # fill in timestamp columns if they aren't specified and the model has appropriately named columns
592
+ if has_timestamp_columns?
593
+ timestamp_column_names.each do |name|
594
+ row[name] = now unless row.key?(name)
595
+ end
596
+ end
597
+
598
+ # interpolate the fixture label
599
+ row.each do |key, value|
600
+ row[key] = label if value == "$LABEL"
601
+ end
602
+
603
+ # generate a primary key if necessary
604
+ if has_primary_key_column? && !row.include?(primary_key_name)
605
+ row[primary_key_name] = DataMapper::Fixtures.identify(label)
606
+ end
607
+
608
+ # If STI is used, find the correct subclass for association reflection
609
+ reflection_class =
610
+ if row.include?(inheritance_column_name)
611
+ row[inheritance_column_name].constantize rescue model_class
612
+ else
613
+ model_class
614
+ end
615
+
616
+ reflect_on_all_associations(reflection_class).each do |association|
617
+ case association.macro
618
+ when :belongs_to
619
+ # Do not replace association name with association foreign key if they are named the same
620
+ fk_name = (association.options[:foreign_key] || "#{association.name}_id").to_s
621
+
622
+ if association.name.to_s != fk_name && value = row.delete(association.name.to_s)
623
+ if association.options[:polymorphic] && value.sub!(/\s*\(([^\)]*)\)\s*$/, "")
624
+ # support polymorphic belongs_to as "label (Type)"
625
+ row[association.foreign_type] = $1
626
+ end
627
+
628
+ row[fk_name] = DataMapper::Fixtures.identify(value)
629
+ end
630
+ when :has_and_belongs_to_many
631
+ if (targets = row.delete(association.name.to_s))
632
+ targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/)
633
+ table_name = association.options[:join_table]
634
+ rows[table_name].concat targets.map { |target|
635
+ { association.foreign_key => row[primary_key_name],
636
+ association.association_foreign_key => DataMapper::Fixtures.identify(target) }
637
+ }
638
+ end
639
+ end
640
+ end
641
+ end
642
+
643
+ row
644
+ end
645
+ rows
646
+ end
647
+
648
+ private
649
+ # Returns primary key name as string, can be composite key with multiple columns
650
+ def primary_key_name
651
+ #TODO don't know that the multiple column thing actually reflects datamapper realities
652
+ @primary_key_name ||= model_class && model_class.key.collect{|c| c.name }.join('-')
653
+ end
654
+
655
+ def has_primary_key_column?
656
+ @has_primary_key_column ||= primary_key_name() &&
657
+ model_class.properties.any? { |c| c.name.to_s == primary_key_name }
658
+ end
659
+
660
+ def has_timestamp_columns?
661
+ !timestamp_column_names.empty?
662
+ end
663
+
664
+ def timestamp_column_names
665
+ # comparison to strings here means all names are in strings
666
+ @timestamp_column_names ||=
667
+ %w(created_at created_on updated_at updated_on) & column_names
668
+ end
669
+
670
+ def inheritance_column_name
671
+ @inheritance_column_name ||= model_class && model_class.properties.select{|prop| prop.kind_of? DataMapper::Property::Discriminator }.collect{|c| c.name.to_s }.first
672
+ end
673
+
674
+ def column_names
675
+ @column_names ||= model_class.properties.collect{|prop| prop.name.to_s } #@connection.columns(@table_name).collect { |c| c.name }
676
+ end
677
+
678
+ # http://pastie.org/pastes/233178
679
+ # Semi-equivalent of ActiveRecord's reflect_on_all_associations method
680
+ def reflect_on_all_associations klass
681
+ klass.relationships.collect do |relationship|
682
+ # All can be child/parent
683
+ # ManyToMany
684
+ # ManyToOne
685
+ # OneToMany
686
+ # OneToOne
687
+ #TODO :has_and_belongs_to_many
688
+ if relationship.kind_of?(::DataMapper::Associations::ManyToOne::Relationship) && relationship.child_model? #relationship.options[:min] == 1 && relationship.options[:max] == 1
689
+ macro = :belongs_to
690
+ if relationship.options[:class_name]
691
+ # In a belongs_to, the side with the class name uses
692
+ # the parent model, but child key for the foreign key...
693
+ class_name = relationship.parent_model.to_s
694
+ primary_key_name = relationship.child_key.entries.first.name
695
+ else
696
+ class_name = relationship.child_model.to_s
697
+ primary_key_name = relationship.parent_key.entries.first.name
698
+ end
699
+ else
700
+ macro = :has_one
701
+ if relationship.options[:class_name]
702
+ # but on the has_one side, it's the *child* model that
703
+ # uses the child key for the foreign key. Weirdness.
704
+ class_name = relationship.child_model.to_s
705
+ primary_key_name = relationship.child_key.entries.first.name
706
+ else
707
+ class_name = relationship.parent_model.to_s
708
+ primary_key_name = relationship.parent_key.entries.first.name
709
+ end
710
+ end
711
+ OpenStruct.new(
712
+ :name => relationship.name,
713
+ :options => {
714
+ #:foreign_key => , # Ignore, let the calling code infer: {name}_id
715
+ :polymorphic => false # Hard code for now
716
+ },
717
+ :class_name => class_name,
718
+ :primary_key_name => primary_key_name,
719
+ :macro => macro
720
+ )
721
+ end
722
+ end
723
+
724
+ def read_fixture_files
725
+ yaml_files = Dir["#{@fixture_path}/**/*.yml"].select { |f|
726
+ ::File.file?(f)
727
+ } + [yaml_file_path]
728
+
729
+ yaml_files.each do |file|
730
+ Fixtures::File.open(file) do |fh|
731
+ fh.each do |name, row|
732
+ fixtures[name] = DataMapper::Fixture.new(row, model_class)
733
+ end
734
+ end
735
+ end
736
+ end
737
+
738
+ def yaml_file_path
739
+ "#{@fixture_path}.yml"
740
+ end
741
+
742
+ end
743
+
744
+ class Fixture #:nodoc:
745
+ include Enumerable
746
+
747
+ class FixtureError < StandardError #:nodoc:
748
+ end
749
+
750
+ class FormatError < FixtureError #:nodoc:
751
+ end
752
+
753
+ attr_reader :model_class, :fixture
754
+
755
+ def initialize(fixture, model_class)
756
+ @fixture = fixture
757
+ @model_class = model_class
758
+ end
759
+
760
+ def class_name
761
+ model_class.name if model_class
762
+ end
763
+
764
+ def each
765
+ fixture.each { |item| yield item }
766
+ end
767
+
768
+ def [](key)
769
+ fixture[key]
770
+ end
771
+
772
+ alias :to_hash :fixture
773
+
774
+ def find
775
+ if model_class
776
+ model_class.find(fixture[model_class.primary_key])
777
+ else
778
+ raise FixtureClassNotFound, "No class attached to find."
779
+ end
780
+ end
781
+ end
782
+ end
783
+
784
+ module DataMapper
785
+ module TestFixtures
786
+ extend ActiveSupport::Concern
787
+
788
+ included do
789
+ setup :setup_fixtures
790
+ teardown :teardown_fixtures
791
+
792
+ class_attribute :fixture_path
793
+ class_attribute :fixture_table_names
794
+ class_attribute :fixture_class_names
795
+ class_attribute :use_transactional_fixtures
796
+ class_attribute :use_instantiated_fixtures # true, false, or :no_instances
797
+ class_attribute :pre_loaded_fixtures
798
+
799
+ self.fixture_table_names = []
800
+ self.use_transactional_fixtures = true
801
+ self.use_instantiated_fixtures = false
802
+ self.pre_loaded_fixtures = false
803
+
804
+ self.fixture_class_names = Hash.new do |h, table_name|
805
+ h[table_name] = DataMapper::Fixtures.find_table_name(table_name)
806
+ end
807
+ end
808
+
809
+ module ClassMethods
810
+ def set_fixture_class(class_names = {})
811
+ self.fixture_class_names = self.fixture_class_names.merge(class_names)
812
+ end
813
+
814
+ def fixtures(*fixture_names)
815
+ if fixture_names.first == :all
816
+ fixture_names = Dir["#{fixture_path}/**/*.{yml}"]
817
+ fixture_names.map! { |f| f[(fixture_path.size + 1)..-5] }
818
+ else
819
+ fixture_names = fixture_names.flatten.map { |n| n.to_s }
820
+ end
821
+
822
+ self.fixture_table_names |= fixture_names
823
+ require_fixture_classes(fixture_names)
824
+ setup_fixture_accessors(fixture_names)
825
+ end
826
+
827
+ def try_to_load_dependency(file_name)
828
+ require_dependency file_name
829
+ rescue LoadError => e
830
+ # Let's hope the developer has included it himself
831
+
832
+ # Let's warn in case this is a subdependency, otherwise
833
+ # subdependency error messages are totally cryptic
834
+ if DataMapper.logger
835
+ DataMapper.logger.push("Unable to load #{file_name}, underlying cause #{e.message} \n\n #{e.backtrace.join("\n")}")
836
+ end
837
+ end
838
+
839
+ def require_fixture_classes(fixture_names = nil)
840
+ (fixture_names || fixture_table_names).each do |fixture_name|
841
+ file_name = fixture_name.to_s
842
+ file_name = file_name.singularize if DataMapper.repository.adapter.resource_naming_convention != DataMapper::NamingConventions::Resource::Underscored
843
+ try_to_load_dependency(file_name)
844
+ end
845
+ end
846
+
847
+ def setup_fixture_accessors(fixture_names = nil)
848
+ fixture_names = Array.wrap(fixture_names || fixture_table_names)
849
+ methods = Module.new do
850
+ fixture_names.each do |fixture_name|
851
+ fixture_name = fixture_name.to_s.tr('./', '_')
852
+
853
+ define_method(fixture_name) do |*fixtures|
854
+ force_reload = fixtures.pop if fixtures.last == true || fixtures.last == :reload
855
+
856
+ @fixture_cache[fixture_name] ||= {}
857
+
858
+ instances = fixtures.map do |fixture|
859
+ @fixture_cache[fixture_name].delete(fixture) if force_reload
860
+
861
+ if @loaded_fixtures[fixture_name][fixture.to_s]
862
+ ActiveRecord::IdentityMap.without do
863
+ @fixture_cache[fixture_name][fixture] ||= @loaded_fixtures[fixture_name][fixture.to_s].find
864
+ end
865
+ else
866
+ raise StandardError, "No fixture with name '#{fixture}' found for table '#{fixture_name}'"
867
+ end
868
+ end
869
+
870
+ instances.size == 1 ? instances.first : instances
871
+ end
872
+ private fixture_name
873
+ end
874
+ end
875
+ include methods
876
+ end
877
+
878
+ def uses_transaction(*methods)
879
+ @uses_transaction = [] unless defined?(@uses_transaction)
880
+ @uses_transaction.concat methods.map { |m| m.to_s }
881
+ end
882
+
883
+ def uses_transaction?(method)
884
+ @uses_transaction = [] unless defined?(@uses_transaction)
885
+ @uses_transaction.include?(method.to_s)
886
+ end
887
+ end
888
+
889
+ def run_in_transaction?
890
+ use_transactional_fixtures && defined? DataMapper::Transaction && #make sure we have dm-transactions
891
+ !self.class.uses_transaction?(method_name)
892
+ end
893
+
894
+ def setup_fixtures
895
+ # Make sure we're connected to a DB
896
+ # return unless !ActiveRecord::Base.configurations.blank?
897
+ return if DataMapper::Repository.adapters.blank?
898
+
899
+ if pre_loaded_fixtures && !use_transactional_fixtures
900
+ raise RuntimeError, 'pre_loaded_fixtures requires use_transactional_fixtures'
901
+ end
902
+
903
+ @fixture_cache = {}
904
+ @fixture_connections = []
905
+ @@already_loaded_fixtures ||= {}
906
+
907
+ # Load fixtures once and begin transaction.
908
+ if run_in_transaction?
909
+ if @@already_loaded_fixtures[self.class]
910
+ @loaded_fixtures = @@already_loaded_fixtures[self.class]
911
+ else
912
+ @loaded_fixtures = load_fixtures
913
+ @@already_loaded_fixtures[self.class] = @loaded_fixtures
914
+ end
915
+ @fixture_connections = enlist_fixture_connections
916
+ @fixture_connections.each do |repository|
917
+ # http://git.io/2QmaGg
918
+ # connection.increment_open_transactions
919
+ # connection.transaction_joinable = false
920
+ # connection.begin_db_transaction
921
+ transaction = DataMapper::Transaction.new(repository)
922
+ transaction.begin
923
+ repository.push_transaction(transaction)
924
+ end
925
+ # Load fixtures for every test.
926
+ else
927
+ DataMapper::Fixtures.reset_cache
928
+ @@already_loaded_fixtures[self.class] = nil
929
+ @loaded_fixtures = load_fixtures
930
+ end
931
+
932
+ # Instantiate fixtures for every test if requested.
933
+ instantiate_fixtures if use_instantiated_fixtures
934
+ end
935
+
936
+ def teardown_fixtures
937
+ # ActiveRecord::Base.configurations.blank?
938
+ return unless defined?(DataMapper) && !DataMapper::Repository.adapters.blank?
939
+
940
+ # Rollback changes if a transaction is active.
941
+ if run_in_transaction?
942
+ @fixture_connections.each do |repository|
943
+ # if connection.open_transactions != 0
944
+ # connection.rollback_db_transaction
945
+ # connection.decrement_open_transactions
946
+ # end
947
+ # adapter = repository.adapter
948
+ while repository.current_transaction
949
+ repository.current_transaction.rollback
950
+ repository.pop_transaction
951
+ end
952
+ end
953
+ @fixture_connections.clear
954
+ else
955
+ DataMapper::Fixtures.reset_cache
956
+ end
957
+
958
+ #
959
+ # ActiveRecord::Base.clear_active_connections!
960
+ end
961
+
962
+ def enlist_fixture_connections
963
+ #ActiveRecord::Base.connection_handler.connection_pools.values.map(&:connection)
964
+ DataMapper::Repository.adapters.values
965
+ #TODO think I actually want DataObjects stuff: https://groups.google.com/d/msg/datamapper/16DJGo9DLSI/oqjWvrnJp3oJ
966
+ # or http://stackoverflow.com/a/11594219/1749924
967
+ end
968
+
969
+ private
970
+ def load_fixtures
971
+ fixtures = DataMapper::Fixtures.create_fixtures(fixture_path, fixture_table_names, fixture_class_names)
972
+ Hash[fixtures.map { |f| [f.name, f] }]
973
+ end
974
+
975
+ # for pre_loaded_fixtures, only require the classes once. huge speed improvement
976
+ @@required_fixture_classes = false
977
+
978
+ def instantiate_fixtures
979
+ if pre_loaded_fixtures
980
+ raise RuntimeError, 'Load fixtures before instantiating them.' if DataMapper::Fixtures.all_loaded_fixtures.empty?
981
+ unless @@required_fixture_classes
982
+ self.class.require_fixture_classes DataMapper::Fixtures.all_loaded_fixtures.keys
983
+ @@required_fixture_classes = true
984
+ end
985
+ DataMapper::Fixtures.instantiate_all_loaded_fixtures(self, load_instances?)
986
+ else
987
+ raise RuntimeError, 'Load fixtures before instantiating them.' if @loaded_fixtures.nil?
988
+ @loaded_fixtures.each_value do |fixture_set|
989
+ DataMapper::Fixtures.instantiate_fixtures(self, fixture_set, load_instances?)
990
+ end
991
+ end
992
+ end
993
+
994
+ def load_instances?
995
+ use_instantiated_fixtures != :no_instances
996
+ end
997
+ end
998
+ end