dm-fixtures 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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