fixtures.rb 20.9 KB
Newer Older
D
Initial  
David Heinemeier Hansson 已提交
1 2
require 'erb'
require 'yaml'
3
require 'csv'
D
Initial  
David Heinemeier Hansson 已提交
4

5
# Fixtures are a way of organizing data that you want to test against; in short, sample data. They come in 3 flavours:
D
Initial  
David Heinemeier Hansson 已提交
6
#
7 8 9
#   1.  YAML fixtures
#   2.  CSV fixtures
#   3.  Single-file fixtures
D
Initial  
David Heinemeier Hansson 已提交
10
#
11
# = YAML fixtures
D
Initial  
David Heinemeier Hansson 已提交
12
#
13 14
# This type of fixture is in YAML format and the preferred default. YAML is a file format which describes data structures
# in a non-verbose, humanly-readable format. It ships with Ruby 1.8.1+.
D
Initial  
David Heinemeier Hansson 已提交
15
#
16 17 18 19
# Unlike single-file fixtures, YAML fixtures are stored in a single file per model, which is place in the directory appointed
# by <tt>Test::Unit::TestCase.fixture_path=(path)</tt> (this is automatically configured for Rails, so you can just
# put your files in <your-rails-app>/test/fixtures/). The fixture file ends with the .yml file extension (Rails example:
# "<your-rails-app>/test/fixtures/web_sites.yml"). The format of a YAML fixture file looks like this:
D
Initial  
David Heinemeier Hansson 已提交
20
#
21 22 23 24
#   rubyonrails:
#     id: 1
#     name: Ruby on Rails
#     url: http://www.rubyonrails.org
D
Initial  
David Heinemeier Hansson 已提交
25
#
26 27 28 29 30
#   google:
#     id: 2
#     name: Google
#     url: http://www.google.com
#
31
# This YAML fixture file includes two fixtures.  Each YAML fixture (ie. record) is given a name and is followed by an
32
# indented list of key/value pairs in the "key: value" format.  Records are separated by a blank line for your viewing
33
# pleasure.
34 35 36
#
# = CSV fixtures
#
D
David Heinemeier Hansson 已提交
37
# Fixtures can also be kept in the Comma Separated Value format. Akin to YAML fixtures, CSV fixtures are stored
38 39
# in a single file, but, instead end with the .csv file extension (Rails example: "<your-rails-app>/test/fixtures/web_sites.csv")
#
40
# The format of this type of fixture file is much more compact than the others, but also a little harder to read by us
41 42 43 44 45 46 47 48 49 50 51
# humans.  The first line of the CSV file is a comma-separated list of field names.  The rest of the file is then comprised
# of the actual data (1 per line).  Here's an example:
#
#   id, name, url
#   1, Ruby On Rails, http://www.rubyonrails.org
#   2, Google, http://www.google.com
#
# Should you have a piece of data with a comma character in it, you can place double quotes around that value.  If you
# need to use a double quote character, you must escape it with another double quote.
#
# Another unique attribute of the CSV fixture is that it has *no* fixture name like the other two formats.  Instead, the
52
# fixture names are automatically generated by deriving the class name of the fixture file and adding an incrementing
53 54 55
# number to the end.  In our example, the 1st fixture would be called "web_site_1" and the 2nd one would be called
# "web_site_2".
#
56
# Most databases and spreadsheets support exporting to CSV format, so this is a great format for you to choose if you
57
# have existing data somewhere already.
58 59
#
# = Single-file fixtures
60 61
#
# This type of fixtures was the original format for Active Record that has since been deprecated in favor of the YAML and CSV formats.
62
# Fixtures for this format are created by placing text files in a sub-directory (with the name of the model) to the directory
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
# appointed by <tt>Test::Unit::TestCase.fixture_path=(path)</tt> (this is automatically configured for Rails, so you can just
# put your files in <your-rails-app>/test/fixtures/<your-model-name>/ -- like <your-rails-app>/test/fixtures/web_sites/ for the WebSite
# model).
#
# Each text file placed in this directory represents a "record".  Usually these types of fixtures are named without
# extensions, but if you are on a Windows machine, you might consider adding .txt as the extension.  Here's what the
# above example might look like:
#
#   web_sites/google
#   web_sites/yahoo.txt
#   web_sites/ruby-on-rails
#
# The file format of a standard fixture is simple.  Each line is a property (or column in db speak) and has the syntax
# of "name => value".  Here's an example of the ruby-on-rails fixture above:
#
#   id => 1
#   name => Ruby on Rails
#   url => http://www.rubyonrails.org
#
# = Using Fixtures
#
# Since fixtures are a testing construct, we use them in our unit and functional tests.  There are two ways to use the
# fixtures, but first lets take a look at a sample unit test found:
#
#   require 'web_site'
#
#   class WebSiteTest < Test::Unit::TestCase
#     def test_web_site_count
#       assert_equal 2, WebSite.count
92
#     end
93
#   end
94 95
#
# As it stands, unless we pre-load the web_site table in our database with two records, this test will fail.  Here's the
96 97 98 99 100 101
# easiest way to add fixtures to the database:
#
#   ...
#   class WebSiteTest < Test::Unit::TestCase
#     fixtures :web_sites # add more by separating the symbols with commas
#   ...
102
#
103
# By adding a "fixtures" method to the test case and passing it a list of symbols (only one is shown here tho), we trigger
D
David Heinemeier Hansson 已提交
104 105
# the testing environment to automatically load the appropriate fixtures into the database before each test.  
# To ensure consistent data, the environment deletes the fixtures before running the load.
106 107 108 109 110 111 112 113 114
#
# In addition to being available in the database, the fixtures are also loaded into a hash stored in an instance variable
# of the test case.  It is named after the symbol... so, in our example, there would be a hash available called
# @web_sites.  This is where the "fixture name" comes into play.
#
# On top of that, each record is automatically "found" (using Model.find(id)) and placed in the instance variable of its name.
# So for the YAML fixtures, we'd get @rubyonrails and @google, which could be interrogated using regular Active Record semantics:
#
#   # test if the object created from the fixture data has the same attributes as the data itself
D
Initial  
David Heinemeier Hansson 已提交
115
#   def test_find
116
#     assert_equal @web_sites["rubyonrails"]["name"], @rubyonrails.name
D
Initial  
David Heinemeier Hansson 已提交
117 118
#   end
#
119 120 121 122
# As seen above, the data hash created from the YAML fixtures would have @web_sites["rubyonrails"]["url"] return
# "http://www.rubyonrails.org" and @web_sites["google"]["name"] would return "Google". The same fixtures, but loaded
# from a CSV fixture file would be accessible via @web_sites["web_site_1"]["name"] == "Ruby on Rails" and have the individual
# fixtures available as instance variables @web_site_1 and @web_site_2.
D
Initial  
David Heinemeier Hansson 已提交
123
#
124 125 126 127 128 129 130 131
# If you do not wish to use instantiated fixtures (usually for performance reasons) there are two options.
#
#   - to completely disable instantiated fixtures:
#       self.use_instantiated_fixtures = false
#
#   - to keep the fixture instance (@web_sites) available, but do not automatically 'find' each instance:
#       self.use_instantiated_fixtures = :no_instances 
#
132 133 134 135 136 137 138 139 140 141
# Even if auto-instantiated fixtures are disabled, you can still access them
# by name via special dynamic methods. Each method has the same name as the
# model, and accepts the name of the fixture to instantiate:
#
#   fixtures :web_sites
#
#   def test_find
#     assert_equal "Ruby on Rails", web_sites(:rubyonrails).name
#   end
#
142
# = Dynamic fixtures with ERb
D
Initial  
David Heinemeier Hansson 已提交
143
#
144 145
# Some times you don't care about the content of the fixtures as much as you care about the volume. In these cases, you can
# mix ERb in with your YAML or CSV fixtures to create a bunch of fixtures for load testing, like:
D
Initial  
David Heinemeier Hansson 已提交
146
#
147 148 149 150 151
# <% for i in 1..1000 %>
# fix_<%= i %>:
#   id: <%= i %>
#   name: guy_<%= 1 %>
# <% end %>
D
Initial  
David Heinemeier Hansson 已提交
152
#
D
David Heinemeier Hansson 已提交
153
# This will create 1000 very simple YAML fixtures.
154 155
#
# Using ERb, you can also inject dynamic values into your fixtures with inserts like <%= Date.today.strftime("%Y-%m-%d") %>.
156
# This is however a feature to be used with some caution. The point of fixtures are that they're stable units of predictable
157 158
# sample data. If you feel that you need to inject dynamic values, then perhaps you should reexamine whether your application
# is properly testable. Hence, dynamic values in fixtures are to be considered a code smell.
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
#
# = Transactional fixtures
#
# TestCases can use begin+rollback to isolate their changes to the database instead of having to delete+insert for every test case. 
# They can also turn off auto-instantiation of fixture data since the feature is costly and often unused.
#
#   class FooTest < Test::Unit::TestCase
#     self.use_transactional_fixtures = true
#     self.use_instantiated_fixtures = false
#   
#     fixtures :foos
#   
#     def test_godzilla
#       assert !Foo.find_all.emtpy?
#       Foo.destroy_all
#       assert Foo.find_all.emtpy?
#     end
#   
#     def test_godzilla_aftermath
#       assert !Foo.find_all.emtpy?
#     end
#   end
#   
# If you preload your test database with all fixture data (probably in the Rakefile task) and use transactional fixtures, 
# then you may omit all fixtures declarations in your test cases since all the data's already there and every case rolls back its changes.
#
185 186 187
# In order to use instantiated fixtures with preloaded data, set +self.pre_loaded_fixtures+ to true. This will provide 
# access to fixture data for every table that has been loaded through fixtures (depending on the value of +use_instantiated_fixtures+)
#
188 189 190 191 192 193
# When *not* to use transactional fixtures: 
#   1. You're testing whether a transaction works correctly. Nested transactions don't commit until all parent transactions commit, 
#      particularly, the fixtures transaction which is begun in setup and rolled back in teardown. Thus, you won't be able to verify 
#      the results of your transaction until Active Record supports nested transactions or savepoints (in progress.) 
#   2. Your database does not support transactions. Every Active Record database supports transactions except MySQL MyISAM. 
#      Use InnoDB, MaxDB, or NDB instead.
D
Initial  
David Heinemeier Hansson 已提交
194
class Fixtures < Hash
195 196
  DEFAULT_FILTER_RE = /\.ya?ml$/

197
  def self.instantiate_fixtures(object, table_name, fixtures, load_instances=true)
198 199 200
    old_logger_level = ActiveRecord::Base.logger.level
    ActiveRecord::Base.logger.level = Logger::ERROR

201
    object.instance_variable_set "@#{table_name.to_s.gsub('.','_')}", fixtures
202 203 204 205 206
    if load_instances
      fixtures.each do |name, fixture|
        if model = fixture.find
          object.instance_variable_set "@#{name}", model
        end
207
      end
D
Initial  
David Heinemeier Hansson 已提交
208
    end
209 210

    ActiveRecord::Base.logger.level = old_logger_level
D
Initial  
David Heinemeier Hansson 已提交
211
  end
212 213 214 215
  
  def self.instantiate_all_loaded_fixtures(object, load_instances=true)
    all_loaded_fixtures.each do |table_name, fixtures|
      Fixtures.instantiate_fixtures(object, table_name, fixtures, load_instances)
216
    end
217 218 219 220
  end
  
  cattr_accessor :all_loaded_fixtures
  self.all_loaded_fixtures = {}
221

D
Initial  
David Heinemeier Hansson 已提交
222 223 224 225 226 227
  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
228

229
      fixtures_map = {}
230
      fixtures = table_names.flatten.map do |table_name|
231 232 233 234
        fixtures_map[table_name] = Fixtures.new(connection, File.split(table_name.to_s).last, File.join(fixtures_directory, table_name.to_s))
      end               
      all_loaded_fixtures.merge! fixtures_map  
      
235
      connection.transaction do
236 237
        fixtures.reverse.each { |fixture| fixture.delete_existing_fixtures }
        fixtures.each { |fixture| fixture.insert_fixtures }
D
Initial  
David Heinemeier Hansson 已提交
238
      end
239
      
240
      reset_sequences(connection, table_names) if connection.is_a?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)
241

D
Initial  
David Heinemeier Hansson 已提交
242 243 244 245 246 247
      return fixtures.size > 1 ? fixtures : fixtures.first
    ensure
      ActiveRecord::Base.logger.level = old_logger_level
    end
  end

248 249 250 251 252 253 254 255
  # Work around for PostgreSQL to have new fixtures created from id 1 and running.
  def self.reset_sequences(connection, table_names)
    table_names.flatten.each do |table|
      table_class = Inflector.classify(table.to_s)
      if Object.const_defined?(table_class)
        pk = eval("#{table_class}::primary_key")
        if pk == 'id'
          connection.execute(
256
            "SELECT setval('#{table.to_s}_id_seq', (SELECT MAX(id) FROM #{table.to_s}), true)", 
257 258 259 260 261 262 263
            'Setting Sequence'
          )
        end
      end
    end
  end

264 265
  attr_reader :table_name

266
  def initialize(connection, table_name, fixture_path, file_filter = DEFAULT_FILTER_RE)
D
Initial  
David Heinemeier Hansson 已提交
267 268 269 270
    @connection, @table_name, @fixture_path, @file_filter = connection, table_name, fixture_path, file_filter
    @class_name = Inflector.classify(@table_name)

    read_fixture_files
271 272 273
  end

  def delete_existing_fixtures
274
    @connection.delete "DELETE FROM #{@table_name}", 'Fixture Delete'
275 276 277 278
  end

  def insert_fixtures
    values.each do |fixture|
279
      @connection.execute "INSERT INTO #{@table_name} (#{fixture.key_list}) VALUES (#{fixture.value_list})", 'Fixture Insert'
280
    end
D
Initial  
David Heinemeier Hansson 已提交
281 282 283 284
  end

  private
    def read_fixture_files
285
      if File.file?(yaml_file_path)
286
        # YAML fixtures
287 288 289 290
        begin
          yaml = YAML::load(erb_render(IO.read(yaml_file_path)))
          yaml.each { |name, data| self[name] = Fixture.new(data, @class_name) } if yaml
        rescue Exception=>boom
291
          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"
D
Initial  
David Heinemeier Hansson 已提交
292
        end
293
      elsif File.file?(csv_file_path)
294 295 296 297 298 299 300 301 302
        # CSV fixtures
        reader = CSV::Reader.create(erb_render(IO.read(csv_file_path)))
        header = reader.shift
        i = 0
        reader.each do |row|
          data = {}
          row.each_with_index { |cell, j| data[header[j].to_s.strip] = cell.to_s.strip }
          self["#{Inflector::underscore(@class_name)}_#{i+=1}"]= Fixture.new(data, @class_name)
        end
303 304
      elsif File.file?(deprecated_yaml_file_path)
        raise Fixture::FormatError, ".yml extension required: rename #{deprecated_yaml_file_path} to #{yaml_file_path}"
D
Initial  
David Heinemeier Hansson 已提交
305
      else
306
        # Standard fixtures
307 308 309 310 311
        Dir.entries(@fixture_path).each do |file|
          path = File.join(@fixture_path, file)
          if File.file?(path) and file !~ @file_filter
            self[file] = Fixture.new(path, @class_name)
          end
D
Initial  
David Heinemeier Hansson 已提交
312 313 314 315 316
        end
      end
    end

    def yaml_file_path
317 318 319 320 321
      "#{@fixture_path}.yml"
    end

    def deprecated_yaml_file_path
      "#{@fixture_path}.yaml"
D
Initial  
David Heinemeier Hansson 已提交
322
    end
323 324 325 326

    def csv_file_path
      @fixture_path + ".csv"
    end
327

D
Initial  
David Heinemeier Hansson 已提交
328 329 330 331 332 333 334 335 336 337 338
    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
339 340 341 342
  class FixtureError < StandardError#:nodoc:
  end
  class FormatError < FixtureError#:nodoc:
  end
D
Initial  
David Heinemeier Hansson 已提交
343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361

  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
362 363
    columns = @fixture.keys.collect{ |column_name| ActiveRecord::Base.connection.quote_column_name(column_name) }
    columns.join(", ")
D
Initial  
David Heinemeier Hansson 已提交
364 365 366 367 368
  end

  def value_list
    @fixture.values.map { |v| ActiveRecord::Base.connection.quote(v).gsub('\\n', "\n").gsub('\\r', "\r") }.join(", ")
  end
369

D
Initial  
David Heinemeier Hansson 已提交
370
  def find
371 372 373 374
    if Object.const_defined?(@class_name)
      klass = Object.const_get(@class_name)
      klass.find(self[klass.primary_key])
    end
D
Initial  
David Heinemeier Hansson 已提交
375
  end
376

D
Initial  
David Heinemeier Hansson 已提交
377 378 379 380
  private
    def read_fixture_file(fixture_file_path)
      IO.readlines(fixture_file_path).inject({}) do |fixture, line|
        # Mercifully skip empty lines.
381
        next if line =~ /^\s*$/
D
Initial  
David Heinemeier Hansson 已提交
382 383 384

        # Use the same regular expression for attributes as Active Record.
        unless md = /^\s*([a-zA-Z][-_\w]*)\s*=>\s*(.+)\s*$/.match(line)
385
          raise FormatError, "#{fixture_file_path}: fixture format error at '#{line}'.  Expecting 'key => value'."
D
Initial  
David Heinemeier Hansson 已提交
386 387 388 389
        end
        key, value = md.captures

        # Disallow duplicate keys to catch typos.
390
        raise FormatError, "#{fixture_file_path}: duplicate '#{key}' in fixture." if fixture[key]
D
Initial  
David Heinemeier Hansson 已提交
391 392 393 394 395 396
        fixture[key] = value.strip
        fixture
      end
    end
end

397 398
module Test #:nodoc:
  module Unit #:nodoc:
399 400
    class TestCase #:nodoc:
      include ClassInheritableAttributes
401

402
      cattr_accessor :fixture_path
403 404
      class_inheritable_accessor :fixture_table_names
      class_inheritable_accessor :use_transactional_fixtures
405 406
      class_inheritable_accessor :use_instantiated_fixtures   # true, false, or :no_instances
      class_inheritable_accessor :pre_loaded_fixtures
407 408 409 410

      self.fixture_table_names = []
      self.use_transactional_fixtures = false
      self.use_instantiated_fixtures = true
411
      self.pre_loaded_fixtures = false
D
Initial  
David Heinemeier Hansson 已提交
412

413 414
      @@already_loaded_fixtures = {}

415
      def self.fixtures(*table_names)
416 417 418 419
        table_names = table_names.flatten
        self.fixture_table_names |= table_names
        require_fixture_classes(table_names)
        setup_fixture_accessors(table_names)
420 421
      end

422 423
      def self.require_fixture_classes(table_names=nil)
        (table_names || fixture_table_names).each do |table_name| 
424
          begin
425
            require Inflector.singularize(table_name.to_s)
426
          rescue LoadError
427
            # Let's hope the developer has included it himself
428 429 430
          end
        end
      end
431

432 433 434 435 436 437 438 439 440 441 442 443
      def self.setup_fixture_accessors(table_names=nil)
        (table_names || fixture_table_names).each do |table_name|
          table_name = table_name.to_s.tr('.','_')
          define_method(table_name) do |fixture, *optionals|
            force_reload = optionals.shift
            @fixture_cache[table_name] ||= Hash.new
            @fixture_cache[table_name][fixture] = nil if force_reload
            @fixture_cache[table_name][fixture] ||= @loaded_fixtures[table_name][fixture.to_s].find
          end
        end
      end

444 445 446 447 448 449 450 451 452 453 454 455 456 457
      def self.uses_transaction(*methods)
        @uses_transaction ||= []
        @uses_transaction.concat methods.map { |m| m.to_s }
      end

      def self.uses_transaction?(method)
        @uses_transaction && @uses_transaction.include?(method.to_s)
      end

      def use_transactional_fixtures?
        use_transactional_fixtures &&
          !self.class.uses_transaction?(method_name)
      end

458
      def setup_with_fixtures
459 460 461 462
        if pre_loaded_fixtures && !use_transactional_fixtures
          raise RuntimeError, 'pre_loaded_fixtures requires use_transactional_fixtures' 
        end

463 464
        @fixture_cache = Hash.new

465
        # Load fixtures once and begin transaction.
466
        if use_transactional_fixtures?
467 468 469 470 471 472
          if @@already_loaded_fixtures[self.class]
            @loaded_fixtures = @@already_loaded_fixtures[self.class]
          else
            load_fixtures
            @@already_loaded_fixtures[self.class] = @loaded_fixtures
          end
473 474 475 476 477
          ActiveRecord::Base.lock_mutex
          ActiveRecord::Base.connection.begin_db_transaction

        # Load fixtures for every test.
        else
478
          @@already_loaded_fixtures[self.class] = nil
479 480 481 482 483 484 485 486 487 488 489
          load_fixtures
        end

        # Instantiate fixtures for every test if requested.
        instantiate_fixtures if use_instantiated_fixtures
      end

      alias_method :setup, :setup_with_fixtures

      def teardown_with_fixtures
        # Rollback changes.
490
        if use_transactional_fixtures?
491 492 493
          ActiveRecord::Base.connection.rollback_db_transaction
          ActiveRecord::Base.unlock_mutex
        end
D
Initial  
David Heinemeier Hansson 已提交
494 495
      end

496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514
      alias_method :teardown, :teardown_with_fixtures

      def self.method_added(method)
        case method.to_s
        when 'setup'
          unless method_defined?(:setup_without_fixtures)
            alias_method :setup_without_fixtures, :setup
            define_method(:setup) do
              setup_with_fixtures
              setup_without_fixtures
            end
          end
        when 'teardown'
          unless method_defined?(:teardown_without_fixtures)
            alias_method :teardown_without_fixtures, :teardown
            define_method(:teardown) do
              teardown_without_fixtures
              teardown_with_fixtures
            end
515 516 517
          end
        end
      end
518

519
      private
520 521
        def load_fixtures
          @loaded_fixtures = {}
522 523 524 525 526 527 528
          fixtures = Fixtures.create_fixtures(fixture_path, fixture_table_names)
          unless fixtures.nil?
            if fixtures.instance_of?(Fixtures)
              @loaded_fixtures[fixtures.table_name] = fixtures
            else
              fixtures.each { |f| @loaded_fixtures[f.table_name] = f }
            end
529
          end
530
        end
531

532 533
        # for pre_loaded_fixtures, only require the classes once. huge speed improvement
        @@required_fixture_classes = false
534

535
        def instantiate_fixtures
536 537 538
          if pre_loaded_fixtures
            raise RuntimeError, 'Load fixtures before instantiating them.' if Fixtures.all_loaded_fixtures.empty?
            unless @@required_fixture_classes
539
              self.class.require_fixture_classes Fixtures.all_loaded_fixtures.keys
540 541 542
              @@required_fixture_classes = true
            end
            Fixtures.instantiate_all_loaded_fixtures(self, load_instances?)
543
          else
544 545 546 547
            raise RuntimeError, 'Load fixtures before instantiating them.' if @loaded_fixtures.nil?
            @loaded_fixtures.each do |table_name, fixtures|
              Fixtures.instantiate_fixtures(self, table_name, fixtures, load_instances?)
            end
548
          end
549
        end
550

551 552 553
        def load_instances?
          use_instantiated_fixtures != :no_instances
        end
D
Initial  
David Heinemeier Hansson 已提交
554
    end
555

556
  end
557
end