ar_attr_lazy 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,107 @@
1
+ require 'rubygems'
2
+
3
+ require 'pp'
4
+
5
+ if ENV["AR_VERSION"]
6
+ gem 'activerecord', "= #{ENV["AR_VERSION"]}"
7
+ end
8
+ require 'activerecord'
9
+ require 'active_record/version'
10
+ ActiveRecord::Base.establish_connection(
11
+ "adapter" => "sqlite3",
12
+ "database" => ":memory:"
13
+ )
14
+
15
+ gem 'mcmire-protest'
16
+ require 'protest'
17
+ gem 'mcmire-matchy'
18
+ require 'matchy'
19
+ gem 'mcmire-mocha'
20
+ require 'mocha'
21
+ require 'mocha-protest-integration'
22
+
23
+ Protest.report_with :documentation
24
+ #Protest::Utils::BacktraceFilter::ESCAPE_PATHS << %r|test/unit| << %r|matchy| << %r|mocha-protest-integration|
25
+ Protest::Utils::BacktraceFilter::ESCAPE_PATHS.clear
26
+
27
+ #------------------------
28
+
29
+ module Protest
30
+ class TestCase
31
+ def full_name
32
+ self.class.description + " " + self.name
33
+ end
34
+
35
+ class TestWrapper #:nodoc:
36
+ attr_reader :name
37
+
38
+ def initialize(type, test_case)
39
+ @type = type
40
+ @test = test_case
41
+ @name = "Global #{@type} for #{test_case.description}"
42
+ end
43
+
44
+ def run(report)
45
+ @test.send("do_global_#{@type}")
46
+ end
47
+
48
+ def full_name
49
+ @name
50
+ end
51
+ end
52
+ end
53
+
54
+ module TestWithErrors
55
+ def file
56
+ file_and_line[0]
57
+ end
58
+
59
+ def line
60
+ file_and_line[1]
61
+ end
62
+
63
+ def file_and_line
64
+ backtrace.find {|x| x =~ %r{^.*/test/(.*_test|test_.*)\.rb} }.split(":")[0..1]
65
+ end
66
+ end
67
+
68
+ module Utils
69
+ module Summaries
70
+ def summarize_errors
71
+ return if failures_and_errors.empty?
72
+
73
+ puts "Failures:"
74
+ puts
75
+
76
+ pad_indexes = failures_and_errors.size.to_s.size
77
+ failures_and_errors.each_with_index do |error, index|
78
+ colorize_as = ErroredTest === error ? :errored : :failed
79
+ # PATCH: test.full_name
80
+ puts " #{pad(index+1, pad_indexes)}) #{test_type(error)} in `#{error.test.full_name}' (on line #{error.line} of `#{error.file}')", colorize_as
81
+ # If error message has line breaks, indent the message
82
+ prefix = "with"
83
+ unless error.error.is_a?(Protest::AssertionFailed) ||
84
+ ((RUBY_VERSION =~ /^1\.9/) ? error.error.is_a?(MiniTest::Assertion) : error.error.is_a?(::Test::Unit::AssertionFailedError))
85
+ prefix << " #{error.error.class}"
86
+ end
87
+ if error.error_message =~ /\n/
88
+ puts indent("#{prefix}: <<", 6 + pad_indexes), colorize_as
89
+ puts indent(error.error_message, 6 + pad_indexes + 2), colorize_as
90
+ puts indent(">>", 6 + pad_indexes), colorize_as
91
+ else
92
+ puts indent("#{prefix} `#{error.error_message}'", 6 + pad_indexes), colorize_as
93
+ end
94
+ indent(error.backtrace, 6 + pad_indexes).each {|backtrace| puts backtrace, colorize_as }
95
+ puts
96
+ end
97
+ end
98
+ end
99
+ end
100
+ end
101
+
102
+ #------------------------
103
+
104
+ require 'matchers'
105
+ require 'factories'
106
+
107
+ require 'mcmire/ar_attr_lazy'
@@ -0,0 +1,172 @@
1
+ module MatchyMatchers
2
+ # Ported from an RSpec matcher
3
+ # from https://rspec.lighthouseapp.com/projects/5645/tickets/896-lambda-should-query-matcher
4
+ # with a few tweaks
5
+ class ArQuery #:nodoc:
6
+ cattr_accessor :executed
7
+
8
+ @@recording_queries = false
9
+ def self.recording_queries?
10
+ @@recording_queries
11
+ end
12
+
13
+ def initialize(test_case, expecteds, &block)
14
+ @test_case = test_case
15
+ @expecteds = expecteds
16
+ @expecteds = [1] if @expecteds.empty?
17
+ @block = block
18
+ end
19
+
20
+ def matches?(given_proc)
21
+ @eval_block = false
22
+ @eval_error = nil
23
+ ArQuery.executed = []
24
+ @@recording_queries = true
25
+
26
+ given_proc.call
27
+
28
+ if @expecteds[0].is_a?(Fixnum)
29
+ @expecteds = @expecteds[0]
30
+ @actuals = ArQuery.executed.length
31
+ @matched = (@actuals == @expecteds)
32
+ else
33
+ # assume that a block was not given
34
+ # PATCH: accept multiple queries
35
+ @expecteds = Array(@expecteds)
36
+ @actuals = @expecteds.map {|query| [query, ArQuery.executed.detect {|sql| query === sql }] }
37
+ @matched = @actuals.all? {|e,a| a }
38
+ end
39
+
40
+ eval_block if @block && @matched && !negative_expectation?
41
+
42
+ @matched && @eval_error.nil?
43
+
44
+ ensure
45
+ #ArQuery.executed = nil
46
+ @@recording_queries = false
47
+ end
48
+
49
+ # This is necessary for interoperability with Matchy
50
+ def fail!(which)
51
+ @test_case.flunk(which ? failure_message_for_should : failure_message_for_should_not)
52
+ end
53
+
54
+ # This is necessary for interoperability with Matchy
55
+ def pass!(which)
56
+ @test_case.assert true
57
+ end
58
+
59
+ def eval_block
60
+ @eval_block = true
61
+ begin
62
+ @block.call(ArQuery.executed)
63
+ rescue Exception => err
64
+ @eval_error = err
65
+ end
66
+ end
67
+
68
+ def failure_message_for_should
69
+ if @eval_error
70
+ @eval_error.message
71
+ elsif @expecteds.is_a?(Fixnum)
72
+ "expected #{@expecteds} to be executed, when in fact #{@actuals} were"
73
+ else
74
+ # PATCH: better error message
75
+ msg = ""
76
+ @actuals.select {|e,a| !a }.each do |expected, _|
77
+ msg << "expected a query with pattern #{expected.inspect} to be executed, but it wasn't\n"
78
+ end
79
+ msg << "All queries executed:\n"
80
+ ArQuery.executed.each do |query|
81
+ msg << " - #{query}\n"
82
+ end
83
+ msg
84
+ end
85
+ end
86
+
87
+ def failure_message_for_should_not
88
+ if @expecteds.is_a?(Fixnum)
89
+ "did not expect #{@expecteds} queries to be executed, but they were"
90
+ else
91
+ # PATCH: better error message
92
+ msg = ""
93
+ @actuals.select {|e,a| a }.each do |_, actual|
94
+ msg << "expected a query with pattern #{actual.inspect} not to be executed, but it was\n"
95
+ end
96
+ msg << "All queries executed:\n"
97
+ ArQuery.executed.each do |query|
98
+ msg << " - #{query}\n"
99
+ end
100
+ msg
101
+ end
102
+ end
103
+
104
+ #def description
105
+ # if @expecteds.is_a?(Fixnum)
106
+ # @expecteds == 1 ? "execute 1 query" : "execute #{@expecteds} queries"
107
+ # else
108
+ # "execute query with pattern #{@expecteds.inspect}"
109
+ # end
110
+ #end
111
+
112
+ # Copied from raise_error
113
+ def negative_expectation?
114
+ @negative_expectation ||= !caller.first(3).find { |s| s =~ /should_not/ }.nil?
115
+ end
116
+ end
117
+
118
+ # :call-seq:
119
+ # response.should query
120
+ # response.should query(expected)
121
+ # response.should query(expected1, expected2)
122
+ # response.should query(expected) { |sql| ... }
123
+ # response.should_not query
124
+ # response.should_not query(expected)
125
+ # response.should_not query(expected1, expected2)
126
+ #
127
+ # Accepts a Fixnum, a String, a Regexp, or an array of Strings or Regexps as arguments.
128
+ #
129
+ # With no args, matches if exactly 1 query is executed.
130
+ # With a Fixnum arg, matches if the number of queries executed equals the given number.
131
+ # With a Regexp arg, matches if any query is executed with the given pattern.
132
+ # With multiple args, matches if all given patterns are matched by all queries executed.
133
+ #
134
+ # Pass an optional block to perform extra verifications of the queries matched.
135
+ # The argument of the block will receive an array of query strings that were executed.
136
+ #
137
+ # == Examples
138
+ #
139
+ # lambda { @object.posts }.should query # same as `should query(1)`
140
+ # lambda { @object.valid? }.should query(0)
141
+ # lambda { @object.save }.should query(3)
142
+ # lambda { @object.line_items }.should query("SELECT DISTINCT")
143
+ # lambda { @object.line_items }.should query(/SELECT DISTINCT/)
144
+ # lambda { @object.line_items }.should query(/SELECT DISTINCT/, /SELECT COUNT\(\*\)/)
145
+ # lambda { @object.line_items }.should query(1) { |sql| sql[0].should =~ /SELECT DISTINCT/ }
146
+ #
147
+ # lambda { @object.posts }.should_not query # same as `should_not query(1)`
148
+ # lambda { @object.valid? }.should_not query(0)
149
+ # lambda { @object.save }.should_not query(3)
150
+ # lambda { @object.line_items }.should_not query(/SELECT DISTINCT/)
151
+ # lambda { @object.line_items }.should_not query(/SELECT DISTINCT/, /SELECT COUNT\(\*\)/)
152
+ #
153
+ def query(*expecteds, &block)
154
+ ArQuery.new(self, expecteds, &block)
155
+ end
156
+
157
+ unless defined?(IGNORED_SQL)
158
+ # From active_record/test/cases/helper.rb :
159
+ ::ActiveRecord::Base.connection.class.class_eval do
160
+ IGNORED_SQL = [/^PRAGMA/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /SHOW FIELDS/]
161
+ def execute_with_query_record(sql, name = nil, &block)
162
+ if ArQuery.recording_queries?
163
+ # PATCH: squeeze and strip
164
+ ArQuery.executed << sql.squeeze(" ").strip unless IGNORED_SQL.any? { |ignore| sql =~ ignore }
165
+ end
166
+ execute_without_query_record(sql, name, &block)
167
+ end
168
+ alias_method_chain :execute, :query_record
169
+ end
170
+ end
171
+ end
172
+ Protest::TestCase.class_eval { include MatchyMatchers }
@@ -0,0 +1,324 @@
1
+ require 'helper'
2
+
3
+ # what about STI? (do the lazy attributes carry over?)
4
+
5
+ Protest.context "for a model that doesn't have lazy attributes" do
6
+ global_setup do
7
+ load File.dirname(__FILE__) + '/setup_migration.rb'
8
+ load File.dirname(__FILE__) + '/setup_tables_for_not_using.rb'
9
+ Account.make! do |account|
10
+ User.make!(:account => account) do |user|
11
+ Avatar.make!(:user => user)
12
+ Post.make!(:author => user) do |post|
13
+ Comment.make!(:post => post)
14
+ post.tags << Tag.make
15
+ post.categories << Category.make
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ global_teardown do
22
+ ObjectSpace.each_object(Class) do |klass|
23
+ Object.remove_class(klass) if klass < ActiveRecord::Base
24
+ end
25
+ end
26
+
27
+ def regex(str)
28
+ Regexp.new(Regexp.escape(str))
29
+ end
30
+
31
+ context "with no associations involved" do
32
+ test "find selects all attributes by default" do
33
+ lambda { Account.find(:first) }.should query(regex(%|SELECT * FROM "accounts"|))
34
+ end
35
+ test "accessing any one attribute doesn't do a query" do
36
+ account = Account.first
37
+ lambda { account.name }.should_not query
38
+ end
39
+ test "find still honors an explicit select option" do
40
+ lambda { Account.find(:first, :select => "name") }.should query(regex(%|SELECT name FROM "accounts"|))
41
+ end
42
+ test "find still honors a select option in a parent scope" do
43
+ lambda {
44
+ Account.send(:with_scope, :find => {:select => "name"}) do
45
+ Account.find(:first)
46
+ end
47
+ }.should query(regex(%|SELECT name FROM "accounts"|))
48
+ end
49
+ if Mcmire::ArAttrLazy.ar_version >= 2.3
50
+ test "find still honors a select option in a default scope" do
51
+ lambda { AccountWithDefaultScope.find(:first) }.should query(regex(%|SELECT name FROM "accounts"|))
52
+ end
53
+ end
54
+ end
55
+
56
+ context "accessing a has_many association" do
57
+ before do
58
+ @post = Post.first
59
+ end
60
+ test "find selects all attributes by default" do
61
+ lambda { @post.comments.find(:first) }.should query(regex(%|SELECT * FROM "comments"|))
62
+ end
63
+ test "find still honors an explicit select option" do
64
+ lambda { @post.comments.find(:first, :select => "name") }.should query(
65
+ regex(%|SELECT name FROM "comments"|)
66
+ )
67
+ end
68
+ test "find still honors a select option in a parent scope" do
69
+ lambda {
70
+ Comment.send(:with_scope, :find => {:select => "name"}) do
71
+ @post.comments.find(:first)
72
+ end
73
+ }.should query(
74
+ regex(%|SELECT name FROM "comments"|)
75
+ )
76
+ end
77
+ if Mcmire::ArAttrLazy.ar_version >= 2.3
78
+ test "find still honors a select option in a default scope" do
79
+ lambda { @post.comments_with_default_scope.find(:first) }.should query(
80
+ regex(%|SELECT name FROM "comments"|)
81
+ )
82
+ end
83
+ end
84
+ test "find still honors a select option in the association definition itself" do
85
+ lambda { @post.comments_with_select.find(:first) }.should query(
86
+ regex(%|SELECT name FROM "comments"|)
87
+ )
88
+ end
89
+ end
90
+
91
+ context "accessing a belongs_to association" do
92
+ test "find selects all attributes by default" do
93
+ post = Post.first
94
+ lambda { post.author }.should query(regex(%|SELECT * FROM "users"|))
95
+ end
96
+ # can't do a find on a belongs_to, so no testing needed for that
97
+ end
98
+
99
+ context "accessing a has_one association" do
100
+ test "find selects all attributes by default" do
101
+ account = Account.first
102
+ lambda { account.user }.should query(regex(%|SELECT * FROM "users"|))
103
+ end
104
+ # can't do a find on a has_one, so no testing needed for that
105
+ end
106
+
107
+ context "accessing a has_and_belongs_to_many association" do
108
+ before do
109
+ @post = Post.first
110
+ end
111
+ test "find selects all attributes by default" do
112
+ lambda { @post.tags.find(:all) }.should query(regex(%|SELECT * FROM "tags"|))
113
+ end
114
+ test "find still honors an explicit select option" do
115
+ lambda { @post.tags.find(:all, :select => "tags.name") }.should query(
116
+ regex(%|SELECT tags.name FROM "tags"|)
117
+ )
118
+ end
119
+ test "find still honors a select option in a parent scope" do
120
+ pending "this fails on Rails 2.3.4"
121
+ lambda {
122
+ Tag.send(:with_scope, :find => {:select => "tags.name"}) do
123
+ @post.tags.find(:all)
124
+ end
125
+ }.should query(
126
+ regex(%|SELECT tags.name FROM "tags"|)
127
+ )
128
+ end
129
+ if Mcmire::ArAttrLazy.ar_version >= 2.3
130
+ test "find still honors a select option in a default scope" do
131
+ pending "this fails on Rails 2.3.4"
132
+ lambda {
133
+ @post.tags_with_default_scope.find(:all)
134
+ }.should query(
135
+ regex(%|SELECT tags.name FROM "tags"|)
136
+ )
137
+ end
138
+ end
139
+ test "find still honors a select option in the association definition itself" do
140
+ lambda {
141
+ @post.tags_with_select.find(:all)
142
+ }.should query(
143
+ regex(%|SELECT tags.name FROM "tags"|)
144
+ )
145
+ end
146
+ end
147
+
148
+ context "accessing a has_many :through association" do
149
+ before do
150
+ @post = Post.first
151
+ end
152
+ test "find selects all attributes by default" do
153
+ lambda { @post.categories.find(:all) }.should query(
154
+ regex(%|SELECT "categories".* FROM "categories"|)
155
+ )
156
+ end
157
+ test "find still honors an explicit select option" do
158
+ lambda { @post.categories.find(:all, :select => "categories.name") }.should query(
159
+ regex(%|SELECT categories.name FROM "categories"|)
160
+ )
161
+ end
162
+ test "find still honors a select option in a parent scope" do
163
+ pending "this fails on Rails 2.3.4"
164
+ lambda {
165
+ Category.send(:with_scope, :find => {:select => "categories.name"}) do
166
+ @post.categories.find(:all)
167
+ end
168
+ }.should query(
169
+ regex(%|SELECT categories.name FROM "categories"|)
170
+ )
171
+ end
172
+ if Mcmire::ArAttrLazy.ar_version >= 2.3
173
+ test "find still honors a select option in a default scope" do
174
+ pending "this fails on Rails 2.3.4"
175
+ lambda {
176
+ @post.categories_with_default_scope.find(:all)
177
+ }.should query(
178
+ regex(%|SELECT categories.name FROM "categories"|)
179
+ )
180
+ end
181
+ end
182
+ test "find still honors a select option in the association definition itself" do
183
+ lambda {
184
+ @post.categories_with_select.find(:all)
185
+ }.should query(
186
+ regex(%|SELECT categories.name FROM "categories"|)
187
+ )
188
+ end
189
+ end
190
+
191
+ # has_one :through didn't work properly prior to 2.3.4 - see LH #2719
192
+ if Mcmire::ArAttrLazy.ar_version >= "2.3.4"
193
+ context "accessing a has_one :through association" do
194
+ test "find selects all attributes by default" do
195
+ account = Account.first
196
+ lambda { account.avatar }.should query(
197
+ regex(%|SELECT "avatars".* FROM "avatars"|)
198
+ )
199
+ end
200
+ # can't do a find on a has_one, so no testing needed for that
201
+ end
202
+ end
203
+
204
+ context "eager loading a has_many association (association preloading)" do
205
+ test "find selects all attributes by default" do
206
+ lambda {
207
+ Post.find(:first, :include => :comments)
208
+ }.should query(regex(%|SELECT * FROM "posts"|), regex(%|SELECT "comments".* FROM "comments"|))
209
+ end
210
+ # can't test for an explicit select since that will force a table join
211
+ # can't test for a scope select since association preloading doesn't honor those
212
+ end
213
+ context "eager loading a has_many association (table join)" do
214
+ test "find selects all attributes by default" do
215
+ lambda {
216
+ Post.find(:first, :include => :comments, :order => "comments.id")
217
+ }.should query(%r{"posts"\."body"}, %r{"comments"\."body"})
218
+ end
219
+ # can't test for an explicit select since that clashes with the table join anyway
220
+ # can't test for a scope for the same reason
221
+ end
222
+
223
+ context "eager loading a has_one association (association preloading)" do
224
+ test "find selects all attributes by default" do
225
+ lambda { Account.find(:first, :include => :user) }.should query(regex(%|SELECT "users".* FROM "users"|))
226
+ end
227
+ # can't test for an explicit select since that will force a table join
228
+ # can't test for a scope select since association preloading doesn't honor those
229
+ end
230
+ context "eager loading a has_one association (table join)" do
231
+ test "find selects all attributes by default" do
232
+ lambda {
233
+ Account.find(:first, :include => :user, :order => "users.id")
234
+ }.should query(%r{"users"\."bio"})
235
+ end
236
+ # can't test for an explicit select since that clashes with the table join anyway
237
+ # can't test for a scope for the same reason
238
+ end
239
+
240
+ context "eager loading a belongs_to association (association preloading)" do
241
+ test "find selects all attributes by default" do
242
+ lambda {
243
+ Post.find(:first, :include => :author)
244
+ }.should query(regex(%|SELECT * FROM "posts"|))
245
+ end
246
+ # can't test for an explicit select since that will force a table join
247
+ # can't test for a scope select since association preloading doesn't honor those
248
+ end
249
+ context "eager loading a belongs_to association (table join)" do
250
+ test "find selects all attributes by default" do
251
+ lambda {
252
+ Post.find(:first, :include => :author, :order => "users.id")
253
+ }.should query(%r{"posts"\."(body|summary)"}, %r{"users"\."bio"})
254
+ end
255
+ # can't test for an explicit select since that clashes with the table join anyway
256
+ # can't test for a scope for the same reason
257
+ end
258
+
259
+ context "eager loading a has_and_belongs_to_many association (association preloading)" do
260
+ test "find selects all attributes by default" do
261
+ lambda {
262
+ Post.find(:first, :include => :tags)
263
+ }.should query(regex(%|SELECT * FROM "posts"|), regex(%|SELECT "tags".*, t0.post_id as the_parent_record_id FROM "tags"|))
264
+ end
265
+ # can't test for an explicit select since that will force a table join
266
+ # can't test for a scope select since association preloading doesn't honor those
267
+ end
268
+ context "eager loading a has_and_belongs_to_many association (table join)" do
269
+ test "find selects all attributes by default" do
270
+ lambda {
271
+ Post.find(:first, :include => :tags, :order => "tags.id")
272
+ }.should query(%r{"posts"\."(body|summary)"}, %r{"tags"\."description"})
273
+ end
274
+ # can't test for an explicit select since that clashes with the table join anyway
275
+ # can't test for a scope for the same reason
276
+ end
277
+
278
+ context "eager loading a has_many :through association (association preloading)" do
279
+ test "find selects all attributes by default" do
280
+ lambda {
281
+ Post.find(:first, :include => :categories)
282
+ }.should query(
283
+ regex(%|SELECT * FROM "categories"|)
284
+ )
285
+ end
286
+ # can't test for an explicit select since that will force a table join
287
+ # can't test for a scope select since association preloading doesn't honor those
288
+ end
289
+ context "eager loading a has_many :through association (table join)" do
290
+ test "find selects all attributes by default" do
291
+ lambda {
292
+ Post.find(:first, :include => :categories, :order => "categories.id")
293
+ }.should query(
294
+ regex(%|SELECT "posts"."id" AS t0_r0, "posts"."type" AS t0_r1, "posts"."author_id" AS t0_r2, "posts"."title" AS t0_r3, "posts"."permalink" AS t0_r4, "posts"."body" AS t0_r5, "posts"."summary" AS t0_r6, "categories"."id" AS t1_r0, "categories"."type" AS t1_r1, "categories"."name" AS t1_r2, "categories"."description" AS t1_r3 FROM "posts"|)
295
+ )
296
+ end
297
+ # can't test for an explicit select since that clashes with the table join anyway
298
+ # can't test for a scope for the same reason
299
+ end
300
+
301
+ # has_one :through didn't work properly prior to 2.3.4 - see LH #2719
302
+ if Mcmire::ArAttrLazy.ar_version >= "2.3.4"
303
+ context "eager loading a has_one :through association (association preloading)" do
304
+ test "find selects all attributes by default" do
305
+ lambda { Account.find(:first, :include => :avatar) }.should query(
306
+ regex(%|SELECT "avatars".* FROM "avatars"|)
307
+ )
308
+ end
309
+ # can't test for an explicit select since that will force a table join
310
+ # can't test for a scope select since association preloading doesn't honor those
311
+ end
312
+ context "eager loading a has_one :through association (table join)" do
313
+ test "find selects all attributes by default" do
314
+ pending "this is failing for some reason!"
315
+ lambda {
316
+ Account.find(:first, :include => :avatar, :order => "avatars.filename")
317
+ }.should query(%|SELECT "accounts"."id" AS t0_r0, "accounts"."name" AS t0_r1, "avatars"."id" AS t1_r0, "avatars"."user_id" AS t1_r1, "avatars"."filename" AS t1_r2, "avatars"."data" AS t1_r3 FROM "accounts"|)
318
+ end
319
+ # can't test for an explicit select since that clashes with the table join anyway
320
+ # can't test for a scope for the same reason
321
+ end
322
+ end
323
+
324
+ end