active_factory 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 [name of plugin creator]
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,17 @@
1
+ MIT-LICENSE
2
+ README.rdoc
3
+ Rakefile
4
+ init.rb
5
+ lib/active_factory.rb
6
+ lib/generators/active_factory/install/USAGE
7
+ lib/generators/active_factory/install/install_generator.rb
8
+ lib/generators/active_factory/install/templates/active_factory.rb
9
+ lib/generators/active_factory/install/templates/define_factories.rb
10
+ lib/hash_struct.rb
11
+ spec/active_factory_define_spec.rb
12
+ spec/active_factory_spec.rb
13
+ spec/active_record_environment_spec.rb
14
+ spec/define_lib_factories.rb
15
+ spec/spec_helper.rb
16
+ spec/support/active_record_environment.rb
17
+ Manifest
@@ -0,0 +1,113 @@
1
+ == ActiveFactory
2
+
3
+ <em>A fixture replacement library. With it your specs will become more declarative, uniform and terse.</em>
4
+
5
+ <b>Feedback will be highly appreciated</b>
6
+
7
+ * {Google group}[http://groups.google.com/group/active_factory]
8
+ * {Factory Definition Reference}[https://github.com/tarasevich/active_factory/wiki/Factory-Definition-Reference]
9
+ * {Models Clause Reference}[https://github.com/tarasevich/active_factory/wiki/Models-Clause-Reference]
10
+
11
+ == Introduction
12
+
13
+ ActiveFactory allows you declaratively define
14
+ which objects you want to have in a database for your spec.
15
+ You can also define associations between the objects and
16
+ redefine default values.
17
+ Additionally ActiveFactory automatically defines accessor methods for the objects.
18
+ The scope of the methods is limited to current spec. So they will not affect you other specs.
19
+
20
+ it "Task.incomplete returns only incomplete tasks" do
21
+ models { project - tasks({:complete => 0}, {:complete => 1}) }
22
+
23
+ project.tasks.incomplete.should == [tasks[0]]
24
+ end
25
+
26
+ it "project displays incomplete tasks" do
27
+ models { my - project - task(:complete => 0) }
28
+
29
+ visit project_path(project)
30
+
31
+ page.should have_content task.title
32
+ end
33
+
34
+ These specs require the following configuration:
35
+
36
+ class ActiveFactory::Define
37
+
38
+ factory :my, :class => User do
39
+ username "my_name"
40
+ password "my_password"
41
+
42
+ before_save do
43
+ model.save!
44
+ context.emulate_sign_in model
45
+ end
46
+ end
47
+
48
+ factory :project do
49
+ title { "Project #{index} title" }
50
+ due { Time.now }
51
+ end
52
+
53
+ factory :task do
54
+ title { "Task #{index} title" }
55
+ end
56
+ end
57
+
58
+ In the configuration you specify default attribute values for an object,
59
+ and in a specific test you may reassign the values for the needs of the test.
60
+ Optional block after_build specifies actions that should be done
61
+ after object was initialized but before saving it.
62
+ If you want to create by the same factory several objects with different values,
63
+ you may use blocks.
64
+ Method index in those blocks returns index of the object being created.
65
+
66
+ == Installation
67
+
68
+ ActiveFactory requires Rails 3 and RSpec 2.
69
+
70
+ rails plugin install git@github.com:tarasevich/active_factory.git
71
+ rails g active_factory:install
72
+
73
+ Now you are ready to add you factories spec/define_factories.rb
74
+ and use models {} block in your specs.
75
+
76
+ <em>Copyright (c) 2010-2011 Alexey Tarasevich, released under the MIT license</em>
77
+
78
+ == Some Examples
79
+
80
+ Consider we have factory declarations from introduction secion.
81
+
82
+ describe ProjectController do
83
+
84
+ it "creates a new project" do
85
+ models { my ; project_ }
86
+
87
+ post :create, :project => project_
88
+
89
+ Project.all.map(&:title).should == [project_[:title]]
90
+ end
91
+ ...
92
+
93
+ In previous example factory +my+ in +models+ block causes to emulate login.
94
+ +project_+ declares corresponding method inside the spec.
95
+ Syntax with uderscore indicates that it's should be just hash
96
+ with values taken from the factory declaration.
97
+ In our case it will be <code>{:title => "Project 0 title"}</code>
98
+
99
+ it "modifies a project's title" do
100
+ models { my - project }
101
+
102
+ put :update, :id => project.id, :project => project_(:title => "New Title")
103
+
104
+ project.reload.title.should == "New Title"
105
+ end
106
+
107
+ In this example we again use +my+ to create a user and log in with it.
108
+ Additionally we create +Project+ instance using +project+ factory
109
+ and define method +project+ locally for the spec.
110
+ Finally we use minus sign to create association between user +my+ and +project+.
111
+ Active factory iterates through associations of +User+ model and chooses the one that
112
+ has +Project+ on the other side.
113
+ <code>project_(h)</code> is just syntax sugar for <code>project_.merge(h)</code>
@@ -0,0 +1,41 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/rdoctask'
4
+ require 'rspec/core/rake_task'
5
+ require 'echoe'
6
+
7
+ desc 'Default: run specs.'
8
+ task :default => :spec
9
+
10
+ desc "Run specs"
11
+ RSpec::Core::RakeTask.new do |t|
12
+ t.pattern = "./spec/**/*_spec.rb" # don't need this, it's default.
13
+ # Put spec opts in a file named .rspec in root
14
+ end
15
+
16
+ desc "Generate code coverage"
17
+ RSpec::Core::RakeTask.new(:coverage) do |t|
18
+ t.pattern = "./spec/**/*_spec.rb" # don't need this, it's default.
19
+ t.rcov = true
20
+ t.rcov_opts = ['--exclude', 'spec']
21
+ end
22
+
23
+ Echoe.new('active_factory', '0.1.0') do |p|
24
+ p.description = "Fixtures replacement with sweet syntax"
25
+ p.url = "http://github.com/tarasevich/active_factory"
26
+ p.author = "Alexey Tarasevich"
27
+ p.ignore_pattern = ["tmp/*", "script/*"]
28
+ p.development_dependencies = []
29
+ end
30
+
31
+ Dir["#{File.dirname(__FILE__)}/tasks/*.rake"].sort.each { |ext| load ext }
32
+
33
+
34
+ desc 'Generate documentation for the active_factory plugin.'
35
+ Rake::RDocTask.new(:rdoc) do |rdoc|
36
+ rdoc.rdoc_dir = 'rdoc'
37
+ rdoc.title = 'ActiveFactory'
38
+ rdoc.options << '--line-numbers' << '--inline-source'
39
+ rdoc.rdoc_files.include('README.rdoc')
40
+ rdoc.rdoc_files.include('lib/**/*.rb')
41
+ end
@@ -0,0 +1,30 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{active_factory}
5
+ s.version = "0.1.0"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Alexey Tarasevich"]
9
+ s.date = %q{2011-04-21}
10
+ s.description = %q{Fixtures replacement with sweet syntax}
11
+ s.email = %q{}
12
+ s.extra_rdoc_files = ["README.rdoc", "lib/active_factory.rb", "lib/generators/active_factory/install/USAGE", "lib/generators/active_factory/install/install_generator.rb", "lib/generators/active_factory/install/templates/active_factory.rb", "lib/generators/active_factory/install/templates/define_factories.rb", "lib/hash_struct.rb"]
13
+ s.files = ["MIT-LICENSE", "README.rdoc", "Rakefile", "init.rb", "lib/active_factory.rb", "lib/generators/active_factory/install/USAGE", "lib/generators/active_factory/install/install_generator.rb", "lib/generators/active_factory/install/templates/active_factory.rb", "lib/generators/active_factory/install/templates/define_factories.rb", "lib/hash_struct.rb", "spec/active_factory_define_spec.rb", "spec/active_factory_spec.rb", "spec/active_record_environment_spec.rb", "spec/define_lib_factories.rb", "spec/spec_helper.rb", "spec/support/active_record_environment.rb", "Manifest", "active_factory.gemspec"]
14
+ s.homepage = %q{http://github.com/tarasevich/active_factory}
15
+ s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Active_factory", "--main", "README.rdoc"]
16
+ s.require_paths = ["lib"]
17
+ s.rubyforge_project = %q{active_factory}
18
+ s.rubygems_version = %q{1.3.7}
19
+ s.summary = %q{Fixtures replacement with sweet syntax}
20
+
21
+ if s.respond_to? :specification_version then
22
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
23
+ s.specification_version = 3
24
+
25
+ if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
26
+ else
27
+ end
28
+ else
29
+ end
30
+ end
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'active_factory' if Rails.env == 'test'
@@ -0,0 +1,480 @@
1
+ require 'hash_struct'
2
+
3
+ module ActiveFactory
4
+ # should be included in specs
5
+ module API
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ attr_accessor :_active_factory_context_extension
10
+
11
+ after do
12
+ _active_factory_context_extension.try :undo
13
+ self._active_factory_context_extension = nil
14
+ end
15
+ end
16
+
17
+ module ClassMethods
18
+ def factory_attributes model_name, index = 0
19
+ Define.
20
+ factories_hash[model_name].
21
+ attributes_for(index)
22
+ end
23
+ end
24
+
25
+ # methods available in specs
26
+ module InstanceMethods
27
+ def models &define_graph
28
+ not _active_factory_context_extension or raise "cannot use models twice in an example"
29
+
30
+ context = self
31
+ factories_hash = Define.factories_hash
32
+ containers_hash = Hash.new { |this, name|
33
+ factory = factories_hash[name]
34
+ this[name] = Container.new(name, factory, context)
35
+ }
36
+ linking_context = LinkingContext.new factories_hash.keys, containers_hash, context
37
+ self._active_factory_context_extension = ContextExtension.new
38
+
39
+ linking_context.instance_eval &define_graph
40
+ containers_hash.values.each &:before_save
41
+ containers_hash.values.each &:save
42
+ _active_factory_context_extension.extend_test_context containers_hash, context
43
+ nil
44
+ end
45
+ end
46
+ end
47
+
48
+ class FactoryDSL
49
+ def initialize
50
+ @attribute_expressions = {}
51
+ end
52
+
53
+ def prefer_associations *assoc_symbols
54
+ @prefer_associations = assoc_symbols
55
+ end
56
+
57
+ def after_build &callback
58
+ @after_build = callback
59
+ end
60
+
61
+ def before_save &callback
62
+ @before_save = callback
63
+ end
64
+
65
+ def method_missing method, *args, &expression
66
+ if args.many? or args.any? and block_given?
67
+ raise "should be either block or value: #{method} #{args.inspect[1..-2]}"
68
+ end
69
+ @attribute_expressions[method.to_sym] = expression || proc { args[0] }
70
+ end
71
+ end
72
+
73
+ # the class that should be "extended" to define models
74
+ class Define
75
+ @@factories = {}
76
+
77
+ def self.factory name, options = {}, &block
78
+ model_class = options[:class]
79
+ if parent_sym = options[:parent]
80
+ parent = @@factories[parent_sym] or raise "undefined parent factory #{parent_sym}"
81
+ end
82
+
83
+ @@factories[name] = FactoryDSL.new.instance_eval {
84
+ instance_eval(&block)
85
+ Factory.new name, parent, model_class,
86
+ @prefer_associations, @attribute_expressions, @after_build, @before_save
87
+ }
88
+ end
89
+
90
+ def self.factories_hash
91
+ @@factories
92
+ end
93
+ end
94
+
95
+ # defines methods that can be used in a model definition
96
+ # model - the model under construction
97
+ # index - index of the model in the factory
98
+ # context - spec context where the models {} block was evaluated
99
+ class CreationContext < Struct.new :index, :context, :model
100
+ alias i index
101
+ end
102
+
103
+ # creates instances of the given model class
104
+ class Factory < Struct.new :name, :parent, :model_class,
105
+ :prefer_associations, :attribute_expressions, :after_build, :before_save
106
+ def initialize name, parent, *overridable
107
+ @overridable = parent ? parent.merge_overridable(overridable) : overridable
108
+ super(name, parent, *@overridable)
109
+ self.attribute_expressions =
110
+ parent.attribute_expressions.merge(self.attribute_expressions) if parent
111
+
112
+ name.is_a? Symbol or raise "factory name #{name.inspect} must be symbol"
113
+ self.model_class ||= (@overridable[0] = Kernel.const_get(name.to_s.capitalize))
114
+ end
115
+
116
+ def merge_overridable overridable
117
+ overridable.zip(@overridable).
118
+ map { |his, my| his or my }
119
+ end
120
+
121
+ def attributes_for index
122
+ context = CreationContext.new(index)
123
+ attrs = attribute_expressions.map { |a, e| [a, context.instance_eval(&e)] }
124
+ Hash[attrs]
125
+ end
126
+
127
+ def apply_after_build index, context, model
128
+ if after_build
129
+ CreationContext.new(index, context, model).
130
+ instance_eval(&after_build)
131
+ end
132
+ end
133
+
134
+ def apply_before_save index, context, model
135
+ if before_save
136
+ CreationContext.new(index, context, model).
137
+ instance_eval(&before_save)
138
+ end
139
+ end
140
+ end
141
+
142
+ class ContainerEntry
143
+ attr_reader :model, :attrs
144
+
145
+ def initialize index, factory, context
146
+ @index = index
147
+ @factory = factory
148
+ @attrs = HashStruct[factory.attributes_for(index)]
149
+ @context = context
150
+ end
151
+
152
+ def merge hash
153
+ @attrs = @attrs.merge hash
154
+ end
155
+
156
+ def build
157
+ unless @model
158
+ @model = @factory.model_class.new
159
+ @attrs.each_pair { |k,v| @model.send "#{k}=", v }
160
+
161
+ @factory.apply_after_build @index, @context, @model
162
+ end
163
+ end
164
+
165
+ def before_save
166
+ if @model and not @saved
167
+ @factory.apply_before_save @index, @context, @model
168
+ end
169
+ end
170
+
171
+ def save
172
+ if @model and not @saved
173
+ @model.save!
174
+ @saved = true
175
+ end
176
+ end
177
+ end
178
+
179
+ # keeps collection of created instances of the given model class
180
+ class Container
181
+ attr_accessor :entries
182
+ attr_reader :name, :factory
183
+
184
+ def initialize name, factory, context
185
+ @name = name
186
+ @factory = factory
187
+ @context = context
188
+ @entries = [].freeze
189
+ end
190
+
191
+ def create count
192
+ dup_with add_entries count
193
+ end
194
+
195
+ def singleton
196
+ if @entries.none?
197
+ add_entries 1
198
+ elsif @entries.many?
199
+ raise "Multiple instances were declared for model :#{@name}."+
200
+ "Use <#{@name.to_s.pluralize}> to access them"
201
+ end
202
+ self
203
+ end
204
+
205
+ def zip_merge *hashes
206
+ @entries.size == hashes.size or raise
207
+
208
+ @entries.zip(hashes) { |entry, hash|
209
+ entry.merge hash
210
+ }
211
+ self
212
+ end
213
+
214
+ def build
215
+ @entries.each &:build
216
+ self
217
+ end
218
+
219
+ def make_linker
220
+ Linker.new self
221
+ end
222
+
223
+ def before_save
224
+ @entries.each &:before_save
225
+ end
226
+
227
+ def save
228
+ @entries.each &:save
229
+ end
230
+
231
+ def attrs
232
+ @entries.map &:attrs
233
+ end
234
+
235
+ def objects
236
+ @entries.map &:model
237
+ end
238
+
239
+ private
240
+
241
+ def dup_with entries
242
+ that = clone
243
+ that.entries = entries
244
+ that
245
+ end
246
+
247
+ def add_entries count
248
+ size = @entries.size
249
+ added = (size...size+count).
250
+ map { |i| ContainerEntry.new i, factory, @context }
251
+ @entries = (@entries + added).freeze
252
+ added
253
+ end
254
+ end
255
+
256
+ # provides syntax to create associations between models
257
+ class Linker
258
+ def initialize container, use_association = nil
259
+ @container = container
260
+ @use_association = use_association
261
+
262
+ @entries = container.entries
263
+ @model_class = container.factory.model_class
264
+ @prefer_associations = container.factory.prefer_associations
265
+ end
266
+
267
+ attr_accessor :entries, :model_class
268
+
269
+ def - that
270
+ case that
271
+ when Linker
272
+ associate that
273
+ that
274
+ when Symbol
275
+ Linker.new @container, that
276
+ else
277
+ raise "cannot associate with #{that.inspect}"
278
+ end
279
+ end
280
+
281
+ private
282
+
283
+ def associate linker
284
+ ar = get_association linker.model_class
285
+
286
+ case ar.macro
287
+ when :has_many, :has_and_belongs_to_many
288
+ assoc_entries = proc { |e, e2|
289
+ e.model.send(ar.name) << e2.model
290
+ }
291
+
292
+ if entries.one? or linker.entries.one?
293
+ entries.each { |e|
294
+ linker.entries.each { |e2|
295
+ assoc_entries[e, e2]
296
+ }
297
+ }
298
+
299
+ elsif entries.size == linker.entries.size
300
+ entries.zip(linker.entries) { |e, e2|
301
+ assoc_entries[e, e2]
302
+ }
303
+
304
+ else
305
+ raise "when linking models, they should be one of this: 1-n, n-1, n-n (e.i. equal number)"
306
+ end
307
+
308
+ when :belongs_to, :has_one
309
+ assoc_entries = proc { |e, e2|
310
+ e.model.send :"#{ar.name}=", e2.model
311
+ }
312
+
313
+ if linker.entries.one?
314
+ entries.each { |e|
315
+ assoc_entries[e, linker.entries.first]
316
+ }
317
+
318
+ elsif entries.size == linker.entries.size
319
+ entries.zip(linker.entries) { |e, e2|
320
+ assoc_entries[e, e2]
321
+ }
322
+
323
+ else
324
+ raise "exactly one instance of an object should be assigned to belongs_to association: #{@container.name} - #{linker.instance_variable_get(:@container).try :name}"
325
+ end
326
+ end
327
+ end
328
+
329
+ def get_association with_class
330
+ if @use_association
331
+ @model_class.reflect_on_association(@use_association) or
332
+ raise "No association #{@use_association.inspect} found for #{@model_class}"
333
+ else
334
+ find_association with_class
335
+ end
336
+ end
337
+
338
+ def find_association with_class
339
+ assocs = @model_class.reflect_on_all_associations.find_all { |assoc|
340
+ assoc.class_name == with_class.name
341
+ }
342
+
343
+ if assocs.none?
344
+ raise "Trying to link, but no association found from #{@model_class} to #{with_class}"
345
+
346
+ elsif assocs.one?
347
+ assocs.first
348
+
349
+ elsif assocs.many?
350
+ resolved = assocs.select { |assoc| @prefer_associations.member? assoc.name }
351
+ resolved.one? or
352
+ raise "Ambiguous associations: #{assocs.map(&:name).inspect} of #{@model_class} to #{with_class}. prefer_associations=#{@prefer_associations.inspect}"
353
+
354
+ resolved.first
355
+ end
356
+ end
357
+
358
+ end
359
+
360
+ # provides methods that refer models in models {} block
361
+ class LinkingContext
362
+ def initialize model_names, containers_hash, context
363
+ @context = context
364
+ h = containers_hash
365
+
366
+ obj_class_eval do
367
+ model_names.each { |name|
368
+
369
+ define_method name do |*args|
370
+ not args.many? or raise "0 or 1 arguments expected, got: #{args.inspect}"
371
+
372
+ if args.none?
373
+ h[name].singleton
374
+ else
375
+ h[name].singleton.zip_merge(args[0])
376
+
377
+ end.build.make_linker
378
+ end
379
+
380
+ define_method :"#{name.to_s.pluralize}" do |*args|
381
+
382
+ if args.none?
383
+ h[name]
384
+
385
+ elsif args[0].is_a? Fixnum and args.one?
386
+ h[name].create(args[0])
387
+
388
+ elsif args.all? { |arg| arg.is_a? Hash }
389
+ h[name].create(args.size).zip_merge(*args)
390
+
391
+ else
392
+ raise "expected no args, or single integer, or several hashes, got: #{args.inspect}"
393
+
394
+ end.build.make_linker
395
+ end
396
+
397
+ define_method :"#{name}_" do
398
+ h[name].singleton.make_linker
399
+ end
400
+
401
+ define_method :"#{name.to_s.pluralize}_" do |count|
402
+ h[name].create(count).make_linker
403
+ end
404
+ }
405
+ end
406
+ end
407
+
408
+ private
409
+
410
+ def obj_class_eval &block
411
+ class << self
412
+ self
413
+ end.class_eval &block
414
+ end
415
+
416
+ def method_missing *args, &block
417
+ if block
418
+ @context.send *args, &block
419
+ else
420
+ @context.send *args
421
+ end
422
+ end
423
+ end
424
+
425
+ # introduces models' names in a spec's context
426
+ class ContextExtension
427
+ def undo
428
+ @undo_define_methods[] if @undo_define_methods
429
+ @undo_define_methods = nil
430
+ end
431
+
432
+ def extend_test_context containers_hash, context
433
+ mrg = proc {|args, hash|
434
+ if args.none?
435
+ hash
436
+ elsif args.one? and args[0].is_a? Hash
437
+ hash.merge args[0]
438
+ else
439
+ raise "Only has is valid argument, but *args=#{args.inspect}"
440
+ end
441
+ }
442
+
443
+ method_defs =
444
+ containers_hash.map { |name, container| [
445
+ name, proc { container.singleton.objects[0] },
446
+ :"#{name}_", proc { |*args| mrg[args, container.singleton.attrs[0]] },
447
+ :"#{name.to_s.pluralize}", proc { container.objects },
448
+ :"#{name.to_s.pluralize}_", proc { container.attrs }
449
+ ] }.
450
+ flatten.each_slice(2)
451
+
452
+ @undo_define_methods =
453
+ define_methods_with_undo context, method_defs
454
+ end
455
+
456
+ private
457
+
458
+ def define_methods_with_undo model, method_defs
459
+ old_methods = model.methods.map &:to_sym
460
+
461
+ overridden_methods, new_methods =
462
+ method_defs.
463
+ map(&:first).
464
+ partition { |name| old_methods.include? name.to_sym }
465
+
466
+ overridden_methods.map! { |name| [name, model.method(name)] }
467
+
468
+ define_method, undef_method = class << model
469
+ [method(:define_method), method(:undef_method)]
470
+ end
471
+
472
+ method_defs.each{ |n,b| define_method[n,b] }
473
+
474
+ lambda {
475
+ overridden_methods.each{ |n,b| define_method[n,b] }
476
+ new_methods.each &undef_method
477
+ }
478
+ end
479
+ end
480
+ end