aboutsummaryrefslogtreecommitdiffstats
path: root/activerecord/lib/active_record/fixtures.rb
blob: f17768e1f206aaa0a8312aa75893d6adb3cf69d9 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
require 'erb'
require 'yaml'
require 'active_record/support/class_inheritable_attributes'
require 'active_record/support/inflector'

# Fixtures are a way of organizing data that you want to test against. You normally have one YAML file with fixture
# definitions per model. They're just hashes of hashes with the first-level key being the name of fixture (try to keep
# that name unique across all fixtures in the system for reasons that will follow). The value to that key is a hash
# where the keys are column names and the values the fixture data you want to insert into it. Example for developers.yml:
#
# david:
#  id: 1
#  name: David Heinemeier Hansson
#  birthday: 1979-10-15
#  profession: Systems development
#
# steve:
#  id: 2
#  name: Steve Ross Kellock
#  birthday: 1974-09-27
#  profession: guy with keyboard
#
# So this YAML file includes two fixtures. T
#
# Now when we call <tt>@developers = Fixtures.create_fixtures(".", "developers")</tt> both developers will get inserted into 
# the "developers" table through the active Active Record connection (that must be setup before-hand). And we can now query 
# the fixture data through the <tt>@developers</tt> hash, so <tt>@developers["david"]["name"]</tt> will return 
# <tt>"David Heinemeier Hansson"</tt> and <tt>@developers["david"]["birthday"]</tt> will return <tt>Date.new(1979, 10, 15)</tt>.
#
# In addition to getting the raw data, we can also get the Developer object by doing @developers["david"].find. This can then 
# be used for comparison in a unit test. Something like:
#
#   def test_find
#     assert_equal @developers["david"]["name"], @developers["david"].find.name
#   end
#
# Comparing that the data we have on the name is also what the object returns when we ask for it.
#
# == Automatic fixture setup and instance variable availability
#
# Fixtures can also be automatically instantiated in instance variables relating to their names using the following style:
#
#   class FixturesTest < Test::Unit::TestCase
#     fixtures :developers # you can add more with comma separation
#
#     def test_developers
#       assert_equal 3, @developers.size # the container for all the fixtures is automatically set
#       assert_kind_of Developer, @david # works like @developers["david"].find
#       assert_equal "David Heinemeier Hansson", @david.name
#     end
#   end
class Fixtures < Hash
  def self.instantiate_fixtures(object, fixtures_directory, *table_names)
    [ create_fixtures(fixtures_directory, *table_names) ].flatten.each_with_index do |fixtures, idx|
      object.instance_variable_set "@#{table_names[idx]}", fixtures
      fixtures.each { |name, fixture| object.instance_variable_set "@#{name}", fixture.find }
    end
  end
  
  def self.create_fixtures(fixtures_directory, *table_names)
    connection = block_given? ? yield : ActiveRecord::Base.connection
    old_logger_level = ActiveRecord::Base.logger.level

    begin
      ActiveRecord::Base.logger.level = Logger::ERROR
      fixtures = connection.transaction do
        table_names.flatten.map do |table_name|
          Fixtures.new(connection, table_name.to_s, File.join(fixtures_directory, table_name.to_s))
        end
      end
      return fixtures.size > 1 ? fixtures : fixtures.first
    ensure
      ActiveRecord::Base.logger.level = old_logger_level
    end
  end

  def initialize(connection, table_name, fixture_path, file_filter = /^\.|CVS|\.yml/)
    @connection, @table_name, @fixture_path, @file_filter = connection, table_name, fixture_path, file_filter
    @class_name = Inflector.classify(@table_name)

    read_fixture_files
    delete_existing_fixtures
    insert_fixtures
  end

  private
    def read_fixture_files
      if File.exists?(yaml_file_path)
        YAML::load(erb_render(IO.read(yaml_file_path))).each do |name, data|
          self[name] = Fixture.new(data, @class_name)
        end
      else
        Dir.entries(@fixture_path).each do |file| 
          self[file] = Fixture.new(File.join(@fixture_path, file), @class_name) unless file =~ @file_filter
        end
      end
    end

    def delete_existing_fixtures
      @connection.delete "DELETE FROM #{@table_name}"
    end

    def insert_fixtures
      values.each do |fixture|
        @connection.execute "INSERT INTO #{@table_name} (#{fixture.key_list}) VALUES(#{fixture.value_list})"
      end
    end

    def yaml_file_path
      @fixture_path + ".yml"
    end
    
    def yaml_fixtures_key(path)
      File.basename(@fixture_path).split(".").first
    end

    def erb_render(fixture_content)
      ERB.new(fixture_content).result
    end
end

class Fixture #:nodoc:
  include Enumerable
  class FixtureError < StandardError; end
  class FormatError < FixtureError; end

  def initialize(fixture, class_name)
    @fixture = fixture.is_a?(Hash) ? fixture : read_fixture_file(fixture)
    @class_name = class_name
  end

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

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

  def to_hash
    @fixture
  end

  def key_list
    @fixture.keys.join(", ")
  end

  def value_list
    @fixture.values.map { |v| ActiveRecord::Base.connection.quote(v).gsub('\\n', "\n").gsub('\\r', "\r") }.join(", ")
  end
  
  def find
    Object.const_get(@class_name).find(self["id"])
  end
  
  private
    def read_fixture_file(fixture_file_path)
      IO.readlines(fixture_file_path).inject({}) do |fixture, line|
        # Mercifully skip empty lines.
        next if line.empty?

        # Use the same regular expression for attributes as Active Record.
        unless md = /^\s*([a-zA-Z][-_\w]*)\s*=>\s*(.+)\s*$/.match(line)
          raise FormatError, "#{path}: fixture format error at '#{line}'.  Expecting 'key => value'."
        end
        key, value = md.captures

        # Disallow duplicate keys to catch typos.
        raise FormatError, "#{path}: duplicate '#{key}' in fixture." if fixture[key]
        fixture[key] = value.strip
        fixture
      end
    end
end

class Test::Unit::TestCase #:nodoc:
  include ClassInheritableAttributes
  
  cattr_accessor :fixture_path
  cattr_accessor :fixture_table_names
  
  def self.fixtures(*table_names)
    write_inheritable_attribute("fixture_table_names", table_names)
  end

  def setup
    instantiate_fixtures(*fixture_table_names) if fixture_table_names
  end
  
  def self.method_added(method_symbol)
    if method_symbol == :setup && !method_defined?(:setup_without_fixtures)
      alias_method :setup_without_fixtures, :setup
      define_method(:setup) do
        instantiate_fixtures(*fixture_table_names) if fixture_table_names
        setup_without_fixtures
      end
    end
  end

  private
    def instantiate_fixtures(*table_names)
      Fixtures.instantiate_fixtures(self, fixture_path, *table_names)
    end
    
    def fixture_table_names
      self.class.read_inheritable_attribute("fixture_table_names")
    end
end