mamiya 0.0.1.alpha19 → 0.0.1.alpha20

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,58 @@
1
+ require 'spec_helper'
2
+ require 'mamiya/agent/tasks/abstract'
3
+
4
+ describe Mamiya::Agent::Tasks::Abstract do
5
+ let(:queue) { double('task_queue') }
6
+
7
+ let(:job) { {'foo' => 'bar'} }
8
+ subject(:task) { described_class.new(queue, job) }
9
+
10
+ describe "#task" do
11
+ before do
12
+ allow(described_class).to receive(:identifier).and_return('ident')
13
+ end
14
+ it "includes job specification and class' identifier" do
15
+ expect(task.task['foo']).to eq 'bar'
16
+ expect(task.task['task']).to eq described_class.identifier
17
+ end
18
+ end
19
+
20
+ describe "#execute" do
21
+ it "calls before, then run" do
22
+ expect(task).to receive(:before).ordered
23
+ expect(task).to receive(:run).ordered
24
+ task.execute
25
+ end
26
+ it "calls after" do
27
+ expect(task).to receive(:run).ordered
28
+ expect(task).to receive(:after).ordered
29
+ expect(task).not_to receive(:error)
30
+ task.execute
31
+ end
32
+
33
+ it "handles error" do
34
+ expect(task).to receive(:after)
35
+ expect(task).to receive(:errored)
36
+
37
+ err = RuntimeError.new
38
+ allow(task).to receive(:run).and_raise(err)
39
+
40
+ task.execute
41
+
42
+ expect(task.error).to eq err
43
+ end
44
+ end
45
+
46
+ describe ".identifier" do
47
+ it "returns class name without module name" do
48
+ allow(described_class).to receive(:name).and_return('Mamiya::Agent::Tasks::Foo')
49
+ expect(described_class.identifier).to eq 'foo'
50
+ end
51
+
52
+ it "returns camelcased class name" do
53
+ allow(described_class).to receive(:name).and_return('Mamiya::Agent::Tasks::FooBar')
54
+ expect(described_class.identifier).to eq 'foo_bar'
55
+ end
56
+ end
57
+ end
58
+
@@ -0,0 +1,72 @@
1
+ require 'spec_helper'
2
+
3
+ require 'mamiya/agent/tasks/clean'
4
+ require 'mamiya/agent/tasks/abstract'
5
+ require 'mamiya/steps/fetch'
6
+
7
+ describe Mamiya::Agent::Tasks::Clean do
8
+ let!(:tmpdir) { Dir.mktmpdir('mamiya-agent-tasks-clean-spec') }
9
+ after { FileUtils.remove_entry_secure(tmpdir) if File.exist?(tmpdir) }
10
+
11
+ let(:config) { {packages_dir: tmpdir, keep_packages: 2} }
12
+
13
+ let(:agent) { double('agent', config: config) }
14
+ let(:task_queue) { double('task_queue') }
15
+
16
+ subject(:task) { described_class.new(task_queue, {}, agent: agent, raise_error: true) }
17
+
18
+ it 'inherits abstract task' do
19
+ expect(described_class.ancestors).to include(Mamiya::Agent::Tasks::Abstract)
20
+ end
21
+
22
+
23
+ describe "#execute" do
24
+ before do
25
+ path = Pathname.new(tmpdir)
26
+
27
+ path.join('a').mkdir
28
+ File.write path.join('a', "a.tar.gz"), "\n"
29
+ File.write path.join('a', "a.json"), "\n"
30
+ File.write path.join('a', "b.json"), "\n"
31
+ File.write path.join('a', "b.tar.gz"), "\n"
32
+ File.write path.join('a', "c.json"), "\n"
33
+ File.write path.join('a', "c.tar.gz"), "\n"
34
+ path.join('b').mkdir
35
+ File.write path.join('b', "a.tar.gz"), "\n"
36
+ File.write path.join('b', "a.json"), "\n"
37
+
38
+ path.join('c').mkdir
39
+ File.write path.join('c', "a.tar.gz"), "\n"
40
+ File.write path.join('c', "b.json"), "\n"
41
+ end
42
+
43
+ it "cleans up" do
44
+ expect(agent).to receive(:trigger).with('pkg', action: 'remove', application: 'a', package: 'a', coalesce: false)
45
+
46
+ task.execute
47
+
48
+ path = Pathname.new(tmpdir)
49
+ existences = Hash[
50
+ [
51
+ path.join('a', 'a.tar.gz'),
52
+ path.join('a', 'a.json'),
53
+ path.join('a', 'b.tar.gz'),
54
+ path.join('a', 'b.json'),
55
+ path.join('a', 'c.tar.gz'),
56
+ path.join('a', 'c.json'),
57
+ ].map { |file|
58
+ [file, file.exist?]
59
+ }
60
+ ]
61
+
62
+ expect(existences).to eq(
63
+ path.join('a', 'a.tar.gz') => false,
64
+ path.join('a', 'a.json') => false,
65
+ path.join('a', 'b.tar.gz') => true,
66
+ path.join('a', 'b.json') => true,
67
+ path.join('a', 'c.tar.gz') => true,
68
+ path.join('a', 'c.json') => true,
69
+ )
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,56 @@
1
+ require 'spec_helper'
2
+
3
+ require 'mamiya/agent/tasks/fetch'
4
+ require 'mamiya/agent/tasks/notifyable'
5
+ require 'mamiya/steps/fetch'
6
+
7
+ describe Mamiya::Agent::Tasks::Fetch do
8
+ let(:config) { {packages_dir: File::NULL} }
9
+ let(:agent) { double('agent', config: config) }
10
+ let(:task_queue) { double('task_queue', enqueue: nil) }
11
+
12
+ let(:step) { double('step', run!: nil) }
13
+
14
+ let(:job) { {'app' => 'myapp', 'pkg' => 'mypkg'} }
15
+
16
+ subject(:task) { described_class.new(task_queue, job, agent: agent, raise_error: true) }
17
+
18
+ it 'inherits notifyable task' do
19
+ expect(described_class.ancestors).to include(Mamiya::Agent::Tasks::Notifyable)
20
+ end
21
+
22
+ describe "#execute" do
23
+ before do
24
+ allow(Mamiya::Steps::Fetch).to receive(:new).with(
25
+ application: 'myapp',
26
+ package: 'mypkg',
27
+ destination: File.join(File::NULL, 'myapp'),
28
+ config: config,
29
+ ).and_return(step)
30
+
31
+ allow(agent).to receive(:trigger)
32
+ end
33
+
34
+ it "calls fetch step" do
35
+ expect(step).to receive(:run!)
36
+
37
+ task.execute
38
+ end
39
+
40
+ it "enqueues clean task" do
41
+ expect(task_queue).to receive(:enqueue).with(:clean, {})
42
+
43
+ task.execute
44
+ end
45
+
46
+ context "when already fetched" do
47
+ it "does nothing" do
48
+ allow(step).to receive(:run!).and_raise(Mamiya::Storages::Abstract::AlreadyFetched)
49
+
50
+ expect {
51
+ task.execute
52
+ }.not_to raise_error
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,37 @@
1
+ require 'spec_helper'
2
+ require 'mamiya/agent/tasks/notifyable'
3
+
4
+ describe Mamiya::Agent::Tasks::Notifyable do
5
+ let(:queue) { double('task_queue') }
6
+ let(:agent) { double('agent', trigger: nil) }
7
+
8
+ # specifying 'task' key that Tasks::Abstract assigns,
9
+ # to make expecting message easier
10
+ let(:job) { {'foo' => 'bar', 'task' => 'notifyable'} }
11
+ subject(:task) { described_class.new(queue, job, agent: agent) }
12
+
13
+ describe "#execute" do
14
+ it "notifies first, then :run" do
15
+ expect(agent).to receive(:trigger).with('task', action: 'start', task: job).ordered
16
+ expect(task).to receive(:before).ordered
17
+ expect(task).to receive(:run).ordered
18
+ task.execute
19
+ end
20
+
21
+ it "calls after, then notify" do
22
+ expect(task).to receive(:run).ordered
23
+ expect(task).to receive(:after).ordered
24
+ expect(agent).to receive(:trigger).with('task', action: 'finish', task: job).ordered
25
+ task.execute
26
+ end
27
+
28
+ it "handles error" do
29
+ expect(agent).to receive(:trigger).with('task', action: 'error', task: job, error: RuntimeError.name)
30
+ err = RuntimeError.new
31
+ allow(task).to receive(:run).and_raise(err)
32
+ task.execute
33
+ expect(task.error).to eq err
34
+ end
35
+ end
36
+ end
37
+
data/spec/agent_spec.rb CHANGED
@@ -7,7 +7,7 @@ require 'villein/event'
7
7
 
8
8
  require 'mamiya/version'
9
9
  require 'mamiya/agent'
10
- require 'mamiya/agent/fetcher'
10
+ require 'mamiya/agent/task_queue'
11
11
  require 'mamiya/agent/actions'
12
12
 
13
13
  require_relative './support/dummy_serf.rb'
@@ -15,12 +15,9 @@ require_relative './support/dummy_serf.rb'
15
15
  describe Mamiya::Agent do
16
16
 
17
17
  let(:serf) { DummySerf.new }
18
- let(:fetcher) do
19
- double('fetcher', start!: nil, working?: false).tap do |f|
20
- cleanup_hook = nil
21
- allow(f).to receive(:cleanup_hook=) { |_| cleanup_hook = _ }
22
- allow(f).to receive(:cleanup_hook) { cleanup_hook }
23
- end
18
+
19
+ let(:task_queue) do
20
+ double('task_queue', start!: nil)
24
21
  end
25
22
 
26
23
  let(:config) do
@@ -29,7 +26,7 @@ describe Mamiya::Agent do
29
26
 
30
27
  before do
31
28
  allow(Villein::Agent).to receive(:new).and_return(serf)
32
- allow(Mamiya::Agent::Fetcher).to receive(:new).and_return(fetcher)
29
+ allow(Mamiya::Agent::TaskQueue).to receive(:new).and_return(task_queue)
33
30
  end
34
31
 
35
32
  subject(:agent) { described_class.new(config) }
@@ -38,32 +35,47 @@ describe Mamiya::Agent do
38
35
  expect(described_class.ancestors).to include(Mamiya::Agent::Actions)
39
36
  end
40
37
 
41
- describe "fetcher" do
42
- it "sends events on cleanup hook" do
38
+ describe "#trigger" do
39
+ it "sends serf event" do
43
40
  expect(serf).to receive(:event).with(
44
- 'mamiya:fetch-result:remove',
41
+ 'mamiya:foo',
45
42
  {
46
- name: serf.name, application: 'foo', package: 'bar',
43
+ a: 'b',
44
+ name: 'my-name',
47
45
  }.to_json,
48
- coalesce: false,
46
+ coalesce: true,
49
47
  )
50
48
 
51
- agent.fetcher.cleanup_hook.call('foo', 'bar')
49
+ agent.trigger(:foo, a: 'b')
50
+ end
51
+
52
+ it "sends serf event with action" do
53
+ expect(serf).to receive(:event).with(
54
+ 'mamiya:foo:bar',
55
+ {
56
+ a: 'b',
57
+ name: 'my-name',
58
+ }.to_json,
59
+ coalesce: true,
60
+ )
61
+
62
+ agent.trigger(:foo, a: 'b', action: 'bar')
52
63
  end
53
64
  end
54
65
 
55
66
  describe "#run!" do
56
- it "starts serf and fetcher" do
67
+ it "starts serf, and task_queue" do
57
68
  begin
58
69
  flag = false
59
70
 
60
- expect(fetcher).to receive(:start!)
71
+ expect(task_queue).to receive(:start!)
61
72
  expect(serf).to receive(:start!)
62
73
  expect(serf).to receive(:auto_stop) do
63
74
  flag = true
64
75
  end
65
76
 
66
77
  th = Thread.new { agent.run! }
78
+ th.abort_on_exception = true
67
79
 
68
80
  10.times { break if flag; sleep 0.1 }
69
81
  ensure
@@ -82,18 +94,6 @@ describe Mamiya::Agent do
82
94
  end
83
95
  end
84
96
 
85
- context "when it is fetching" do
86
- before do
87
- allow(fetcher).to receive(:working?).and_return(true)
88
- end
89
-
90
- it "shows fetching" do
91
- agent.update_tags!
92
-
93
- expect(serf.tags['mamiya']).to eq ',fetching,'
94
- end
95
- end
96
-
97
97
  context "when it is running multiple jobs" do
98
98
  pending
99
99
  end
@@ -111,10 +111,8 @@ describe Mamiya::Agent do
111
111
  describe "#status" do
112
112
  before do
113
113
  allow(agent).to receive(:existing_packages).and_return("app" => ["pkg"])
114
- allow(fetcher).to receive(:queue_size).and_return(42)
115
- allow(fetcher).to receive(:working?).and_return(false)
116
- allow(fetcher).to receive(:current_job).and_return(nil)
117
- allow(fetcher).to receive(:pending_jobs).and_return([['app', 'pkg2', nil, nil]])
114
+
115
+ allow(task_queue).to receive(:status).and_return({a: {working: nil, queue: []}})
118
116
  end
119
117
 
120
118
  subject(:status) { agent.status }
@@ -131,28 +129,9 @@ describe Mamiya::Agent do
131
129
  expect(status[:packages]).to eq agent.existing_packages
132
130
  end
133
131
 
134
- describe "(fetcher)" do
135
- it "includes queue_size as pending" do
136
- expect(status[:fetcher][:pending]).to eq 42
137
- end
138
-
139
- it "includes pendings job" do
140
- expect(status[:fetcher][:pending_jobs]).to eq([['app', 'pkg2']])
141
- end
142
-
143
- it "shows fetching status" do
144
- expect(status[:fetcher][:fetching]).to be_nil
145
- end
146
-
147
- context "when it's fetching" do
148
- before do
149
- allow(fetcher).to receive(:working?).and_return(true)
150
- allow(fetcher).to receive(:current_job).and_return(%w(foo bar))
151
- end
152
-
153
- it "shows fetching true" do
154
- expect(status[:fetcher][:fetching]).to eq ['foo', 'bar']
155
- end
132
+ describe "(task queue)" do
133
+ it "includes task_queue" do
134
+ expect(status[:queues]).to eq({a: {working: nil, queue: []}})
156
135
  end
157
136
  end
158
137
  end
@@ -150,119 +150,205 @@ describe Mamiya::Master::AgentMonitor do
150
150
 
151
151
  subject(:new_status) { agent_monitor.statuses["a"] }
152
152
 
153
- describe "fetch-result" do
154
- describe ":ack" do
155
- let(:status) do
156
- {fetcher: {fetching: nil, pending: 0}}
157
- end
153
+ describe "task" do
154
+ describe ":start" do
155
+ context "if task is in the queue" do
156
+ let(:status) do
157
+ {queues: {
158
+ a: {queue: [{task: 'a', foo: 'bar'}], working: nil}
159
+ }}
160
+ end
158
161
 
159
- it "updates pending" do
160
- commit('mamiya:fetch-result:ack', pending: 72, application: 'foo', package: 'bar')
161
- expect(new_status["fetcher"]["pending"]).to eq 72
162
- expect(new_status["fetcher"]["pending_jobs"]).to eq [%w(foo bar)]
163
- end
164
- end
162
+ it "removes from queue, set in working" do
163
+ commit('mamiya:task:start',
164
+ task: {task: 'a', foo: 'bar'})
165
165
 
166
- describe ":start" do
167
- let(:status) do
168
- {fetcher: {fetching: nil, pending: 1, pending_jobs: [%w(app pkg)]}}
166
+ expect(new_status['queues']['a']['queue']).to be_empty
167
+ expect(new_status['queues']['a']['working']).to eq('task' => 'a', 'foo' => 'bar')
168
+ end
169
169
  end
170
170
 
171
- it "updates fetching" do
172
- commit('mamiya:fetch-result:start',
173
- application: 'app', package: 'pkg', pending: 0)
174
- expect(new_status["fetcher"]["fetching"]).to eq ['app', 'pkg']
175
- expect(new_status["fetcher"]["pending_jobs"]).to be_empty
171
+ context "if task is not in the queue" do
172
+ let(:status) do
173
+ {queues: {
174
+ a: {queue: [], working: nil}
175
+ }}
176
+ end
177
+
178
+ it "set in working" do
179
+ commit('mamiya:task:start',
180
+ task: {task: 'a', foo: 'bar'})
181
+
182
+ expect(new_status['queues']['a']['working']).to eq('task' => 'a', 'foo' => 'bar')
183
+ end
176
184
  end
177
185
  end
178
186
 
179
- describe ":error" do
180
- let(:status) do
181
- {fetcher: {fetching: ['app', 'pkg'], pending: 0}}
187
+ describe ":finish" do
188
+ context "if task is working" do
189
+ let(:status) do
190
+ {queues: {
191
+ a: {queue: [], working: {task: 'a', foo: 'bar'}}
192
+ }}
193
+ end
194
+
195
+ it "removes from working" do
196
+ commit('mamiya:task:finish',
197
+ task: {task: 'a', foo: 'bar'})
198
+
199
+ expect(new_status['queues']['a']['working']).to be_nil
200
+ end
182
201
  end
183
202
 
184
- it "updates fetching" do
185
- commit('mamiya:fetch-result:error',
186
- application: 'app', package: 'pkg', pending: 0)
203
+ context "if task is in queue" do
204
+ let(:status) do
205
+ {queues: {
206
+ a: {queue: [{task: 'a', foo: 'bar'}, {task: 'a', bar: 'baz'}], working: nil}
207
+ }}
208
+ end
209
+
210
+ it "removes from queue" do
211
+ commit('mamiya:task:finish',
212
+ task: {task: 'a', foo: 'bar'})
187
213
 
188
- expect(new_status["fetcher"]["fetching"]).to eq nil
214
+ expect(new_status['queues']['a']['working']).to be_nil
215
+ expect(new_status['queues']['a']['queue']).to eq [{'task' => 'a', 'bar' => 'baz'}]
216
+ end
189
217
  end
190
218
 
191
- context "when package doesn't match with present state" do
192
- it "doesn't updates fetching" do
193
- commit('mamiya:fetch-result:error',
194
- application: 'app', package: 'pkg2', pending: 0)
219
+ context "if task is not working" do
220
+ let(:status) do
221
+ {queues: {
222
+ a: {queue: [], working: {task: 'a', foo: 'baz'}}
223
+ }}
224
+ end
225
+
226
+ it "does nothing" do
227
+ commit('mamiya:task:finish',
228
+ task: {task: 'a', foo: 'bar'})
195
229
 
196
- expect(new_status["fetcher"]["fetching"]).to \
197
- eq(['app', 'pkg'])
230
+ expect(new_status['queues']['a']['working']).to eq('task' => 'a', 'foo' => 'baz')
231
+ expect(new_status['queues']['a']['queue']).to eq []
198
232
  end
199
233
  end
200
234
  end
201
235
 
202
- describe ":success" do
203
- let(:status) do
204
- {fetcher: {fetching: ['app', 'pkg'], pending: 0},
205
- packages: {}}
206
- end
236
+ describe ":error" do
237
+ context "if task is working" do
238
+ let(:status) do
239
+ {queues: {
240
+ a: {queue: [], working: {task: 'a', foo: 'bar'}}
241
+ }}
242
+ end
207
243
 
208
- it "updates fetching" do
209
- commit('mamiya:fetch-result:success',
210
- application: 'app', package: 'pkg', pending: 0)
244
+ it "removes from working" do
245
+ commit('mamiya:task:finish',
246
+ task: {task: 'a', foo: 'bar'})
211
247
 
212
- expect(new_status["fetcher"]["fetching"]).to eq nil
248
+ expect(new_status['queues']['a']['working']).to be_nil
249
+ end
213
250
  end
214
251
 
215
- it "updates packages" do
216
- commit('mamiya:fetch-result:success',
217
- application: 'app', package: 'pkg', pending: 0)
252
+ context "if task is in queue" do
253
+ let(:status) do
254
+ {queues: {
255
+ a: {queue: [{task: 'a', foo: 'bar'}, {task: 'a', bar: 'baz'}], working: nil}
256
+ }}
257
+ end
218
258
 
219
- expect(new_status["packages"]["app"]).to eq ["pkg"]
259
+ it "removes from queue" do
260
+ commit('mamiya:task:finish',
261
+ task: {task: 'a', foo: 'bar'})
262
+
263
+ expect(new_status['queues']['a']['working']).to be_nil
264
+ expect(new_status['queues']['a']['queue']).to eq [{'task' => 'a', 'bar' => 'baz'}]
265
+ end
220
266
  end
221
267
 
222
- context "with existing packages" do
268
+ context "if task is not working" do
223
269
  let(:status) do
224
- {fetcher: {fetching: ['app', 'pkg2'], pending: 0},
225
- packages: {"app" => ['pkg1']}}
270
+ {queues: {
271
+ a: {queue: [], working: {task: 'a', foo: 'baz'}}
272
+ }}
226
273
  end
227
274
 
228
- it "updates packages" do
229
- commit('mamiya:fetch-result:success',
230
- application: 'app', package: 'pkg2', pending: 0)
275
+ it "does nothing" do
276
+ commit('mamiya:task:finish',
277
+ task: {task: 'a', foo: 'bar'})
231
278
 
232
- expect(new_status["packages"]["app"]).to eq %w(pkg1 pkg2)
279
+ expect(new_status['queues']['a']['working']).to eq('task' => 'a', 'foo' => 'baz')
280
+ expect(new_status['queues']['a']['queue']).to eq []
233
281
  end
234
282
  end
283
+ end
284
+ end
235
285
 
236
- context "when package doesn't match with present state" do
237
- it "doesn't updates fetching" do
238
- commit('mamiya:fetch-result:success',
239
- application: 'app', package: 'pkg2', pending: 0)
286
+ describe "(task handling)" do
287
+ describe "pkg" do
288
+ describe ":remove" do
289
+ let(:status) do
290
+ {packages: {'myapp' => ['pkg1']}}
291
+ end
292
+
293
+ it "removes removed package from packages" do
294
+ commit('mamiya:pkg:remove',
295
+ application: 'myapp', package: 'pkg1')
240
296
 
241
- expect(agent_monitor.statuses["a"]["fetcher"]["fetching"]).to \
242
- eq(['app', 'pkg'])
297
+ expect(new_status["packages"]['myapp']).to eq []
243
298
  end
244
299
 
245
- it "updates packages" do
246
- commit('mamiya:fetch-result:success',
247
- application: 'app', package: 'pkg', pending: 0)
300
+ context "with existing packages" do
301
+ let(:status) do
302
+ {packages: {'myapp' => ['pkg1', 'pkg2']}}
303
+ end
304
+
305
+ it "removes removed package from packages" do
306
+ commit('mamiya:pkg:remove',
307
+ application: 'myapp', package: 'pkg1')
308
+
309
+ expect(new_status["packages"]['myapp']).to eq ['pkg2']
310
+ end
311
+ end
248
312
 
249
- expect(new_status["packages"]["app"]).to eq ["pkg"]
313
+ context "with inexist package" do
314
+ let(:status) do
315
+ {packages: {'myapp' => ['pkg1', 'pkg3']}}
316
+ end
317
+
318
+ it "removes removed package from packages" do
319
+ commit('mamiya:pkg:remove',
320
+ application: 'myapp', package: 'pkg2')
321
+
322
+ expect(new_status["packages"]['myapp']).to eq ['pkg1', 'pkg3']
323
+ end
250
324
  end
251
325
  end
252
326
  end
253
327
 
254
- describe ":remove" do
255
- context "with existing packages" do
328
+ describe "fetch" do
329
+ describe "success" do
256
330
  let(:status) do
257
- {fetcher: {fetching: ['app', 'pkg2'], pending: 0},
258
- packages: {"app" => ['pkg1']}}
331
+ {packages: {}}
259
332
  end
260
333
 
261
334
  it "updates packages" do
262
- commit('mamiya:fetch-result:remove',
263
- application: 'app', package: 'pkg1', pending: 0)
335
+ commit('mamiya:task:finish',
336
+ task: {task: 'fetch', app: 'myapp', pkg: 'pkg'})
337
+
338
+ expect(new_status["packages"]['myapp']).to eq ["pkg"]
339
+ end
340
+
341
+ context "with existing packages" do
342
+ let(:status) do
343
+ {packages: {'myapp' => ['pkg1']}}
344
+ end
345
+
346
+ it "updates packages" do
347
+ commit('mamiya:task:finish',
348
+ task: {task: 'fetch', app: 'myapp', pkg: 'pkg2'})
264
349
 
265
- expect(new_status["packages"]["app"]).to eq []
350
+ expect(new_status["packages"]['myapp']).to eq %w(pkg1 pkg2)
351
+ end
266
352
  end
267
353
  end
268
354
  end