Ticket #1911: more_flexible_fixture_architecture-3b.patch
| File more_flexible_fixture_architecture-3b.patch, 22.5 kB (added by duane.johnson@gmail.com, 4 years ago) |
|---|
-
lib/active_record/fixtures.rb
old new 2 2 require 'yaml' 3 3 require 'csv' 4 4 5 # A FixtureGroup is a set of fixtures identified by a name. Normally, this is the name of the 6 # corresponding fixture filename. For example, when you declare the use of fixtures in a 7 # TestUnit class, like so: 8 # fixtures :users 9 # you are creating a FixtureGroup whose name is 'users', and whose defaults are set such that the 10 # +class_name+, +file_name+ and +table_name+ are guessed from the FixtureGroup's name. 11 class FixtureGroup 12 attr_accessor :table_name, :class_name, :connection 13 attr_reader :group_name, :file_name 14 15 def initialize(file_name, optional_names = {}) 16 self.file_name = file_name 17 self.group_name = optional_names[:group_name] || file_name 18 if optional_names[:table_name] 19 self.table_name = optional_names[:table_name] 20 self.class_name = optional_names[:class_name] || Inflector.classify(@table_name.to_s.gsub('.','_')) 21 elsif optional_names[:class_name] 22 self.class_name = optional_names[:class_name] 23 if Object.const_defined?(@class_name) 24 model_class = Object.const_get(@class_name) 25 self.table_name = ActiveRecord::Base.table_name_prefix + model_class.table_name + ActiveRecord::Base.table_name_suffix 26 end 27 end 28 29 # In case either :table_name or :class_name was not set: 30 self.table_name ||= ActiveRecord::Base.table_name_prefix + @group_name.to_s + ActiveRecord::Base.table_name_suffix 31 self.class_name ||= Inflector.classify(@table_name.to_s.gsub('.','_')) 32 end 33 34 def file_name=(name) 35 @file_name = name.to_s 36 end 37 38 def group_name=(name) 39 @group_name = name.to_sym 40 end 41 42 def class_file_name 43 Inflector.underscore(@class_name) 44 end 45 46 # Instantiate an array of FixtureGroup objects from an array of strings (table_names) 47 def self.array_from_names(names) 48 names.collect { |n| FixtureGroup.new(n) } 49 end 50 51 def hash 52 @group_name.hash 53 end 54 55 def eql?(other) 56 @group_name.eql? other.group_name 57 end 58 end 59 5 60 # Fixtures are a way of organizing data that you want to test against; in short, sample data. They come in 3 flavours: 6 61 # 7 62 # 1. YAML fixtures … … 193 248 # Use InnoDB, MaxDB, or NDB instead. 194 249 class Fixtures < Hash 195 250 DEFAULT_FILTER_RE = /\.ya?ml$/ 251 252 cattr_accessor :all_loaded_fixtures 253 self.all_loaded_fixtures = {} 196 254 197 def self.instantiate_fixtures(object, table_name, fixtures, load_instances=true) 198 old_logger_level = ActiveRecord::Base.logger.level 199 ActiveRecord::Base.logger.level = Logger::ERROR 255 class << self 256 def instantiate_fixtures(object, fixture_group_name, fixtures, load_instances=true) 257 old_logger_level = ActiveRecord::Base.logger.level 258 ActiveRecord::Base.logger.level = Logger::ERROR 200 259 201 object.instance_variable_set "@#{table_name.to_s.gsub('.','_')}", fixtures 202 if load_instances 203 fixtures.each do |name, fixture| 204 if model = fixture.find 205 object.instance_variable_set "@#{name}", model 260 # table_name.to_s.gsub('.','_') replaced by 'fixture_group_name' 261 object.instance_variable_set "@#{fixture_group_name}", fixtures 262 if load_instances 263 fixtures.each do |name, fixture| 264 if model = fixture.find 265 object.instance_variable_set "@#{name}", model 266 end 206 267 end 207 268 end 269 270 ActiveRecord::Base.logger.level = old_logger_level 208 271 end 209 272 210 ActiveRecord::Base.logger.level = old_logger_level 211 end 212 213 def self.instantiate_all_loaded_fixtures(object, load_instances=true) 214 all_loaded_fixtures.each do |table_name, fixtures| 215 Fixtures.instantiate_fixtures(object, table_name, fixtures, load_instances) 273 def instantiate_all_loaded_fixtures(object, load_instances=true) 274 all_loaded_fixtures.each do |fixture_group_name, fixtures| 275 Fixtures.instantiate_fixtures(object, fixture_group_name, fixtures, load_instances) 276 end 216 277 end 217 end218 219 cattr_accessor :all_loaded_fixtures220 self.all_loaded_fixtures = {}221 278 222 def self.create_fixtures(fixtures_directory, *table_names) 223 connection = block_given? ? yield : ActiveRecord::Base.connection 224 old_logger_level = ActiveRecord::Base.logger.level 279 def create_fixtures(fixtures_directory, *fixture_groups) 280 connection = block_given? ? yield : ActiveRecord::Base.connection 281 old_logger_level = ActiveRecord::Base.logger.level 282 fixture_groups.flatten! 283 284 # Backwards compatibility: Allow an array of table names to be passed in, but just use them 285 # to create an array of FixtureGroup objects 286 if not fixture_groups.empty? and fixture_groups.first.is_a?(String) 287 fixture_groups = FixtureGroup.array_from_names(fixture_groups) 288 end 225 289 226 begin227 ActiveRecord::Base.logger.level = Logger::ERROR290 begin 291 ActiveRecord::Base.logger.level = Logger::ERROR 228 292 229 fixtures_map = {} 230 fixtures = table_names.flatten.map do |table_name| 231 fixtures_map[table_name] = Fixtures.new(connection, File.split(table_name.to_s).last, File.join(fixtures_directory, table_name.to_s)) 232 end 233 all_loaded_fixtures.merge! fixtures_map 234 235 236 connection.transaction do 237 fixtures.reverse.each { |fixture| fixture.delete_existing_fixtures } 238 fixtures.each { |fixture| fixture.insert_fixtures } 293 fixtures_map = {} 294 fixtures = fixture_groups.map do |group| 295 fixtures_map[group.group_name] = Fixtures.new(connection, fixtures_directory, group) 296 end 297 # Make sure all refs to all_loaded_fixtures use group_name as hash index, not table_name 298 all_loaded_fixtures.merge! fixtures_map 299 300 connection.transaction do 301 fixtures.reverse.each { |fixture| fixture.delete_existing_fixtures } 302 fixtures.each { |fixture| fixture.insert_fixtures } 303 end 304 305 reset_sequences(connection, fixture_groups) if connection.is_a?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter) 306 307 return fixtures.size > 1 ? fixtures : fixtures.first 308 ensure 309 ActiveRecord::Base.logger.level = old_logger_level 239 310 end 240 241 reset_sequences(connection, table_names) if connection.is_a?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)242 243 return fixtures.size > 1 ? fixtures : fixtures.first244 ensure245 ActiveRecord::Base.logger.level = old_logger_level246 311 end 247 end248 312 249 # Start PostgreSQL fixtures at id 1. Skip tables without models 250 # and models with nonstandard primary keys. 251 def self.reset_sequences(connection, table_names) 252 table_names.flatten.each do |table| 253 if table_class = table.to_s.classify.constantize rescue nil 254 pk = table_class.columns_hash[table_class.primary_key] 255 if pk and pk.type == :integer 256 connection.execute( 257 "SELECT setval('#{table}_#{pk.name}_seq', (SELECT COALESCE(MAX(#{pk.name}), 0)+1 FROM #{table}), false)", 258 'Setting Sequence' 259 ) 313 # Start PostgreSQL fixtures at id 1. Skip tables without models 314 # and models with nonstandard primary keys. 315 def reset_sequences(connection, fixture_groups) 316 fixture_groups.flatten.each do |group| 317 if klass = group.class_name.constantize rescue nil 318 pk = klass.columns_hash[klass.primary_key] 319 if pk and pk.type == :integer 320 connection.execute( 321 "SELECT setval('#{group.table_name}_#{pk.name}_seq', (SELECT COALESCE(MAX(#{pk.name}), 0)+1 FROM #{group.table_name}), false)", 322 'Setting Sequence' 323 ) 324 end 260 325 end 261 326 end 262 327 end 263 328 end 264 329 265 attr_reader :table_name 330 attr_accessor :connection, :fixtures_directory, :file_filter 331 attr_accessor :fixture_group 266 332 267 def initialize(connection, table_name, fixture_path, file_filter = DEFAULT_FILTER_RE) 268 @connection, @table_name, @fixture_path, @file_filter = connection, table_name, fixture_path, file_filter 269 270 @class_name = Inflector.classify(@table_name) 271 @table_name = ActiveRecord::Base.table_name_prefix + @table_name + ActiveRecord::Base.table_name_suffix 333 def initialize(connection, fixtures_directory, fixture_group, file_filter = DEFAULT_FILTER_RE) 334 @connection, @fixtures_directory = connection, fixtures_directory 335 @fixture_group = fixture_group 336 @file_filter = file_filter 272 337 read_fixture_files 273 338 end 274 339 275 340 def delete_existing_fixtures 276 @connection.delete "DELETE FROM #{@ table_name}", 'Fixture Delete'341 @connection.delete "DELETE FROM #{@fixture_group.table_name}", 'Fixture Delete' 277 342 end 278 343 279 344 def insert_fixtures 280 345 values.each do |fixture| 281 @connection.execute "INSERT INTO #{@ table_name} (#{fixture.key_list}) VALUES (#{fixture.value_list})", 'Fixture Insert'346 @connection.execute "INSERT INTO #{@fixture_group.table_name} (#{fixture.key_list}) VALUES (#{fixture.value_list})", 'Fixture Insert' 282 347 end 283 348 end 284 349 285 350 private 286 351 def read_fixture_files 287 352 if File.file?(yaml_file_path) 288 # YAML fixtures 289 begin 290 yaml = YAML::load(erb_render(IO.read(yaml_file_path))) 291 yaml.each { |name, data| self[name] = Fixture.new(data, @class_name) } if yaml 292 rescue Exception=>boom 293 raise Fixture::FormatError, "a YAML error occured 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 #{boom.class}: #{boom}" 294 end 353 read_yaml_fixture_files 295 354 elsif File.file?(csv_file_path) 296 # CSV fixtures 297 reader = CSV::Reader.create(erb_render(IO.read(csv_file_path))) 298 header = reader.shift 299 i = 0 300 reader.each do |row| 301 data = {} 302 row.each_with_index { |cell, j| data[header[j].to_s.strip] = cell.to_s.strip } 303 self["#{Inflector::underscore(@class_name)}_#{i+=1}"]= Fixture.new(data, @class_name) 304 end 355 read_csv_fixture_files 305 356 elsif File.file?(deprecated_yaml_file_path) 306 357 raise Fixture::FormatError, ".yml extension required: rename #{deprecated_yaml_file_path} to #{yaml_file_path}" 358 elsif File.directory?(single_file_fixtures_path) 359 read_standard_fixture_files 307 360 else 308 # Standard fixtures 309 Dir.entries(@fixture_path).each do |file| 310 path = File.join(@fixture_path, file) 311 if File.file?(path) and file !~ @file_filter 312 self[file] = Fixture.new(path, @class_name) 313 end 361 raise Fixture::FixtureError, "Couldn't find a yaml, csv or standard file to load at #{@fixtures_directory} (#{@fixture_group.file_name})." 362 end 363 end 364 365 def read_yaml_fixture_files 366 # YAML fixtures 367 begin 368 yaml = YAML::load(erb_render(IO.read(yaml_file_path))) 369 yaml.each { |name, data| self[name] = Fixture.new(data, @fixture_group.class_name) } if yaml 370 rescue Exception=>boom 371 raise Fixture::FormatError, "a YAML error occured 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 #{boom.class}: #{boom}" 372 end 373 end 374 375 def read_csv_fixture_files 376 # CSV fixtures 377 reader = CSV::Reader.create(erb_render(IO.read(csv_file_path))) 378 header = reader.shift 379 i = 0 380 reader.each do |row| 381 data = {} 382 row.each_with_index { |cell, j| data[header[j].to_s.strip] = cell.to_s.strip } 383 self["#{@fixture_group.class_file_name}_#{i+=1}"]= Fixture.new(data, @fixture_group.class_name) 384 end 385 end 386 387 def read_standard_fixture_files 388 # Standard fixtures 389 path = File.join(@fixtures_directory, @fixture_group.file_name) 390 Dir.entries(path).each do |file| 391 path = File.join(@fixtures_directory, @fixture_group.file_name, file) 392 if File.file?(path) and file !~ @file_filter 393 self[file] = Fixture.new(path, @fixture_group.class_name) 314 394 end 315 395 end 316 396 end 317 397 318 398 def yaml_file_path 319 "#{@fixture_path}.yml"399 fixture_path_with_extension ".yml" 320 400 end 321 401 322 402 def deprecated_yaml_file_path 323 "#{@fixture_path}.yaml"403 fixture_path_with_extension ".yaml" 324 404 end 325 405 326 406 def csv_file_path 327 @fixture_path +".csv"407 fixture_path_with_extension ".csv" 328 408 end 329 330 def yaml_fixtures_key(path)331 File.basename(@fixture_path).split(".").first409 410 def single_file_fixtures_path 411 fixture_path_with_extension "" 332 412 end 333 413 414 def fixture_path_with_extension(ext) 415 File.join(@fixtures_directory, @fixture_group.file_name + ext) 416 end 417 334 418 def erb_render(fixture_content) 335 419 ERB.new(fixture_content).result 336 420 end … … 399 483 module Test #:nodoc: 400 484 module Unit #:nodoc: 401 485 class TestCase #:nodoc: 402 cattr_accessor :fixture_path 486 cattr_accessor :fixtures_directory 487 class_inheritable_accessor :fixture_groups 403 488 class_inheritable_accessor :fixture_table_names 404 489 class_inheritable_accessor :use_transactional_fixtures 405 490 class_inheritable_accessor :use_instantiated_fixtures # true, false, or :no_instances 406 491 class_inheritable_accessor :pre_loaded_fixtures 407 492 408 self.fixture_ table_names = []493 self.fixture_groups = [] 409 494 self.use_transactional_fixtures = false 410 495 self.use_instantiated_fixtures = true 411 496 self.pre_loaded_fixtures = false 412 497 413 498 @@already_loaded_fixtures = {} 414 499 415 def self.fixtures(*table_names) 416 table_names = table_names.flatten 417 self.fixture_table_names |= table_names 418 require_fixture_classes(table_names) 419 setup_fixture_accessors(table_names) 500 # Backwards compatibility 501 def self.fixture_path=(path); self.fixtures_directory = path; end 502 def self.fixture_path; self.fixtures_directory; end 503 def fixture_group_names; fixture_groups.collect { |g| g.group_name }; end 504 def fixture_table_names; fixture_group_names; end 505 506 def self.fixture(file_name, options = {}) 507 self.fixture_groups |= [FixtureGroup.new(file_name, options)] 508 require_fixture_classes 509 setup_fixture_accessors 420 510 end 421 511 422 def self.require_fixture_classes(table_names=nil) 423 (table_names || fixture_table_names).each do |table_name| 512 def self.fixtures(*file_names) 513 self.fixture_groups |= FixtureGroup.array_from_names(file_names.flatten) 514 require_fixture_classes 515 setup_fixture_accessors 516 end 517 518 def self.require_fixture_classes(fixture_groups_override = nil) 519 (fixture_groups_override || fixture_groups).each do |group| 424 520 begin 425 require Inflector.singularize(table_name.to_s)521 require group.class_file_name 426 522 rescue LoadError 427 523 # Let's hope the developer has included it himself 428 524 end 429 525 end 430 526 end 431 527 432 def self.setup_fixture_accessors(table_names=nil) 433 (table_names || fixture_table_names).each do |table_name| 434 table_name = table_name.to_s.tr('.','_') 435 define_method(table_name) do |fixture, *optionals| 528 def self.setup_fixture_accessors(fixture_groups_override=nil) 529 (fixture_groups_override || fixture_groups).each do |group| 530 define_method(group.group_name) do |fixture, *optionals| 436 531 force_reload = optionals.shift 437 @fixture_cache[ table_name] ||= Hash.new438 @fixture_cache[ table_name][fixture] = nil if force_reload439 @fixture_cache[ table_name][fixture] ||= @loaded_fixtures[table_name][fixture.to_s].find532 @fixture_cache[group.group_name] ||= Hash.new 533 @fixture_cache[group.group_name][fixture] = nil if force_reload 534 @fixture_cache[group.group_name][fixture] ||= @loaded_fixtures[group.group_name][fixture.to_s].find 440 535 end 441 536 end 442 537 end … … 519 614 private 520 615 def load_fixtures 521 616 @loaded_fixtures = {} 522 fixtures = Fixtures.create_fixtures(fixture _path, fixture_table_names)617 fixtures = Fixtures.create_fixtures(fixtures_directory, fixture_groups) 523 618 unless fixtures.nil? 524 619 if fixtures.instance_of?(Fixtures) 525 @loaded_fixtures[fixtures. table_name] = fixtures620 @loaded_fixtures[fixtures.fixture_group.group_name] = fixtures 526 621 else 527 fixtures.each { |f| @loaded_fixtures[f. table_name] = f }622 fixtures.each { |f| @loaded_fixtures[f.fixture_group.group_name] = f } 528 623 end 529 624 end 530 625 end … … 536 631 if pre_loaded_fixtures 537 632 raise RuntimeError, 'Load fixtures before instantiating them.' if Fixtures.all_loaded_fixtures.empty? 538 633 unless @@required_fixture_classes 539 self.class.require_fixture_classes Fixtures.all_loaded_fixtures.keys 634 groups = Fixtures.all_loaded_fixtures.values.collect { |f| f.group_name } 635 self.class.require_fixture_classes groups 540 636 @@required_fixture_classes = true 541 637 end 542 638 Fixtures.instantiate_all_loaded_fixtures(self, load_instances?) 543 639 else 544 640 raise RuntimeError, 'Load fixtures before instantiating them.' if @loaded_fixtures.nil? 545 @loaded_fixtures.each do | table_name, fixtures|546 Fixtures.instantiate_fixtures(self, table_name, fixtures, load_instances?)641 @loaded_fixtures.each do |fixture_group_name, fixtures| 642 Fixtures.instantiate_fixtures(self, fixture_group_name, fixtures, load_instances?) 547 643 end 548 644 end 549 645 end -
test/fixtures/topics2.yml
old new 1 third: 2 id: 3 3 title: The Third Topic 4 author_name: Duane 5 author_email_address: david@loudthinking.com 6 written_on: 2003-07-16t15:28:00.00+01:00 7 last_read: 2004-04-15 8 bonus_time: 2005-01-30t15:28:00.00+01:00 9 content: Have a nice day 10 approved: 0 11 replies_count: 0 12 13 fourth: 14 id: 4 15 title: The Fourth topic 16 author_name: John 17 written_on: 2003-07-15t15:28:00.00+01:00 18 content: Have a nice day 19 approved: 1 20 replies_count: 2 21 parent_id: 1 -
test/fixtures_test.rb
old new 106 106 end 107 107 end 108 108 109 def test_fixtures_not_found 110 assert_raise(Fixture::FixtureError) { 111 Fixtures.new(nil, File.join(File.dirname(__FILE__), 'fixtures'), FixtureGroup.new("bad_extension")) 112 } 113 end 114 109 115 def test_deprecated_yaml_extension 110 116 assert_raise(Fixture::FormatError) { 111 Fixtures.new(nil, 'bad_extension', File.join(File.dirname(__FILE__), 'fixtures'))117 Fixtures.new(nil, File.join(File.dirname(__FILE__), 'fixtures', 'bad_fixtures'), FixtureGroup.new("deprecated")) 112 118 } 113 119 end 114 120 … … 139 145 end 140 146 141 147 def test_empty_yaml_fixture 142 assert_not_nil Fixtures.new( Account.connection, "accounts", File.dirname(__FILE__) + "/fixtures/naked/yml/accounts")148 assert_not_nil Fixtures.new( Account.connection, File.dirname(__FILE__) + "/fixtures/naked/yml/", FixtureGroup.new("accounts")) 143 149 end 144 150 145 151 def test_empty_yaml_fixture_with_a_comment_in_it 146 assert_not_nil Fixtures.new( Account.connection, "companies", File.dirname(__FILE__) + "/fixtures/naked/yml/companies")152 assert_not_nil Fixtures.new( Account.connection, File.dirname(__FILE__) + "/fixtures/naked/yml/", FixtureGroup.new("companies")) 147 153 end 148 154 149 155 def test_dirty_dirty_yaml_file 150 156 assert_raises(Fixture::FormatError) do 151 Fixtures.new( Account.connection, "courses", File.dirname(__FILE__) + "/fixtures/naked/yml/courses")157 Fixtures.new( Account.connection, File.dirname(__FILE__) + "/fixtures/naked/yml/", FixtureGroup.new("courses")) 152 158 end 153 159 end 154 160 155 161 def test_empty_csv_fixtures 156 assert_not_nil Fixtures.new( Account.connection, "accounts", File.dirname(__FILE__) + "/fixtures/naked/csv/accounts")162 assert_not_nil Fixtures.new( Account.connection, File.dirname(__FILE__) + "/fixtures/naked/csv/", FixtureGroup.new("accounts")) 157 163 end 158 164 end 159 165 … … 250 256 251 257 end 252 258 259 class FixturesGroupTest < Test::Unit::TestCase 260 def test_get_table_name_from_model_class 261 fg = FixtureGroup.new(:filedoesnotexist, :class_name => "DeveloperWithAggregate") 262 assert_equal fg.table_name, 'developers' 263 assert_equal fg.class_name, 'DeveloperWithAggregate' 264 end 265 266 def test_assume_correct_class_name_from_table_name 267 fg = FixtureGroup.new(:developers, :table_name => "tests") 268 assert_equal fg.table_name, 'tests' 269 assert_equal fg.class_name, 'Test' 270 end 253 271 272 def test_file_name_is_a_string 273 fg = FixtureGroup.new(:developers) 274 assert String === fg.file_name 275 end 276 277 def test_fixture_group_name 278 fg = FixtureGroup.new(:developers) 279 assert Symbol === fg.group_name 280 assert_equal fg.group_name, :developers 281 282 fg = FixtureGroup.new(:developers, :group_name => "other") 283 assert Symbol === fg.group_name 284 assert_equal fg.group_name, :other 285 end 286 end 254 287 288 class MultipleFixturesForTheSameTableTest < Test::Unit::TestCase 289 self.use_instantiated_fixtures = true 290 fixture :topics, :class_name => "Topic" 291 fixture :topics2, :class_name => "Topic" 292 293 def test_loaded_fixtures 294 assert_equal @topics.size, 2 295 assert_equal @topics2.size, 2 296 union = Topic.find(:all) 297 assert_equal union.size, 4 298 end 299 end 255 300 301 class MixSingularAndPluralFixturesTest < Test::Unit::TestCase 302 self.use_instantiated_fixtures = true 303 fixtures :topics, :developers, :accounts 304 fixture :topics2, :table_name => "topics" 305 306 def test_loaded_fixtures 307 assert_equal @topics.size, 2 308 assert_equal @topics2.size, 2 309 union = Topic.find(:all) 310 assert_equal union.size, 4 311 end 312 end 256 313 257 314 class AlternateFileFixturesTest < Test::Unit::TestCase 315 self.use_instantiated_fixtures = true 316 fixture :topics2, :class_name => "Topic" 317 318 def test_alternate_fixture_file_was_loaded 319 assert_equal @topics2.size, 2 320 assert_nil @first 321 assert_nil @second 322 assert_equal @third.id, 3 323 assert_equal @fourth.id, 4 324 topics = Topic.find(:all) 325 assert_equal @topics2.size, 2 326 end 327 end