librarian-chef 0.0.1.beta.1

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,451 @@
1
+ require 'pathname'
2
+ require 'securerandom'
3
+
4
+ require 'librarian'
5
+ require 'librarian/helpers'
6
+ require 'librarian/error'
7
+ require 'librarian/action/resolve'
8
+ require 'librarian/action/install'
9
+ require 'librarian/action/update'
10
+ require 'librarian/chef'
11
+
12
+ module Librarian
13
+ module Chef
14
+ module Source
15
+ describe Git do
16
+
17
+ let(:project_path) do
18
+ project_path = Pathname.new(__FILE__).expand_path
19
+ project_path = project_path.dirname until project_path.join("Rakefile").exist?
20
+ project_path
21
+ end
22
+ let(:tmp_path) { project_path.join("tmp/spec/integration/chef/source/git") }
23
+ after { tmp_path.rmtree if tmp_path && tmp_path.exist? }
24
+
25
+ let(:cookbooks_path) { tmp_path.join("cookbooks") }
26
+
27
+ # depends on repo_path being defined in each context
28
+ let(:env) { Environment.new(:project_path => repo_path) }
29
+
30
+ context "a single dependency with a git source" do
31
+
32
+ let(:sample_path) { tmp_path.join("sample") }
33
+ let(:sample_metadata) do
34
+ Helpers.strip_heredoc(<<-METADATA)
35
+ version "0.6.5"
36
+ METADATA
37
+ end
38
+
39
+ let(:first_sample_path) { cookbooks_path.join("first-sample") }
40
+ let(:first_sample_metadata) do
41
+ Helpers.strip_heredoc(<<-METADATA)
42
+ version "3.2.1"
43
+ METADATA
44
+ end
45
+
46
+ let(:second_sample_path) { cookbooks_path.join("second-sample") }
47
+ let(:second_sample_metadata) do
48
+ Helpers.strip_heredoc(<<-METADATA)
49
+ version "4.3.2"
50
+ METADATA
51
+ end
52
+
53
+ before do
54
+ sample_path.rmtree if sample_path.exist?
55
+ sample_path.mkpath
56
+ sample_path.join("metadata.rb").open("wb") { |f| f.write(sample_metadata) }
57
+ Dir.chdir(sample_path) do
58
+ `git init`
59
+ `git config user.name "Simba"`
60
+ `git config user.email "simba@savannah-pride.gov"`
61
+ `git add metadata.rb`
62
+ `git commit -m "Initial commit."`
63
+ end
64
+
65
+ cookbooks_path.rmtree if cookbooks_path.exist?
66
+ cookbooks_path.mkpath
67
+ first_sample_path.mkpath
68
+ first_sample_path.join("metadata.rb").open("wb") { |f| f.write(first_sample_metadata) }
69
+ second_sample_path.mkpath
70
+ second_sample_path.join("metadata.rb").open("wb") { |f| f.write(second_sample_metadata) }
71
+ Dir.chdir(cookbooks_path) do
72
+ `git init`
73
+ `git config user.name "Simba"`
74
+ `git config user.email "simba@savannah-pride.gov"`
75
+ `git add .`
76
+ `git commit -m "Initial commit."`
77
+ end
78
+ end
79
+
80
+ context "resolving" do
81
+ let(:repo_path) { tmp_path.join("repo/resolve") }
82
+ before do
83
+ repo_path.rmtree if repo_path.exist?
84
+ repo_path.mkpath
85
+ repo_path.join("cookbooks").mkpath
86
+ cheffile = Helpers.strip_heredoc(<<-CHEFFILE)
87
+ #!/usr/bin/env ruby
88
+ cookbook "sample", :git => #{sample_path.to_s.inspect}
89
+ CHEFFILE
90
+ repo_path.join("Cheffile").open("wb") { |f| f.write(cheffile) }
91
+ end
92
+
93
+ context "the resolve" do
94
+ it "should not raise an exception" do
95
+ expect { Action::Resolve.new(env).run }.to_not raise_error
96
+ end
97
+ end
98
+
99
+ context "the results" do
100
+ before { Action::Resolve.new(env).run }
101
+
102
+ it "should create the lockfile" do
103
+ repo_path.join("Cheffile.lock").should exist
104
+ end
105
+
106
+ it "should not attempt to install the sample cookbok" do
107
+ repo_path.join("cookbooks/sample").should_not exist
108
+ end
109
+ end
110
+ end
111
+
112
+ context "installing" do
113
+ let(:repo_path) { tmp_path.join("repo/install") }
114
+ before do
115
+ repo_path.rmtree if repo_path.exist?
116
+ repo_path.mkpath
117
+ repo_path.join("cookbooks").mkpath
118
+ cheffile = Helpers.strip_heredoc(<<-CHEFFILE)
119
+ #!/usr/bin/env ruby
120
+ cookbook "sample", :git => #{sample_path.to_s.inspect}
121
+ CHEFFILE
122
+ repo_path.join("Cheffile").open("wb") { |f| f.write(cheffile) }
123
+
124
+ Action::Resolve.new(env).run
125
+ end
126
+
127
+ context "the install" do
128
+ it "should not raise an exception" do
129
+ expect { Action::Install.new(env).run }.to_not raise_error
130
+ end
131
+ end
132
+
133
+ context "the results" do
134
+ before { Action::Install.new(env).run }
135
+
136
+ it "should create the lockfile" do
137
+ repo_path.join("Cheffile.lock").should exist
138
+ end
139
+
140
+ it "should create the directory for the cookbook" do
141
+ repo_path.join("cookbooks/sample").should exist
142
+ end
143
+
144
+ it "should copy the cookbook files into the cookbook directory" do
145
+ repo_path.join("cookbooks/sample/metadata.rb").should exist
146
+ end
147
+ end
148
+ end
149
+
150
+ context "resolving and and separately installing" do
151
+ let(:repo_path) { tmp_path.join("repo/resolve-install") }
152
+ before do
153
+ repo_path.rmtree if repo_path.exist?
154
+ repo_path.mkpath
155
+ repo_path.join("cookbooks").mkpath
156
+ cheffile = Helpers.strip_heredoc(<<-CHEFFILE)
157
+ #!/usr/bin/env ruby
158
+ cookbook "sample", :git => #{sample_path.to_s.inspect}
159
+ CHEFFILE
160
+ repo_path.join("Cheffile").open("wb") { |f| f.write(cheffile) }
161
+
162
+ Action::Resolve.new(env).run
163
+ repo_path.join("tmp").rmtree if repo_path.join("tmp").exist?
164
+ end
165
+
166
+ context "the install" do
167
+ it "should not raise an exception" do
168
+ expect { Action::Install.new(env).run }.to_not raise_error
169
+ end
170
+ end
171
+
172
+ context "the results" do
173
+ before { Action::Install.new(env).run }
174
+
175
+ it "should create the directory for the cookbook" do
176
+ repo_path.join("cookbooks/sample").should exist
177
+ end
178
+
179
+ it "should copy the cookbook files into the cookbook directory" do
180
+ repo_path.join("cookbooks/sample/metadata.rb").should exist
181
+ end
182
+ end
183
+ end
184
+
185
+ context "resolving, changing, and resolving" do
186
+ let(:repo_path) { tmp_path.join("repo/resolve-update") }
187
+ before do
188
+ repo_path.rmtree if repo_path.exist?
189
+ repo_path.mkpath
190
+ repo_path.join("cookbooks").mkpath
191
+ cheffile = Helpers.strip_heredoc(<<-CHEFFILE)
192
+ git #{cookbooks_path.to_s.inspect}
193
+ cookbook "first-sample"
194
+ CHEFFILE
195
+ repo_path.join("Cheffile").open("wb") { |f| f.write(cheffile) }
196
+ Action::Resolve.new(env).run
197
+
198
+ cheffile = Helpers.strip_heredoc(<<-CHEFFILE)
199
+ git #{cookbooks_path.to_s.inspect}
200
+ cookbook "first-sample"
201
+ cookbook "second-sample"
202
+ CHEFFILE
203
+ repo_path.join("Cheffile").open("wb") { |f| f.write(cheffile) }
204
+ end
205
+
206
+ context "the second resolve" do
207
+ it "should not raise an exception" do
208
+ expect { Action::Resolve.new(env).run }.to_not raise_error
209
+ end
210
+ end
211
+ end
212
+
213
+ end
214
+
215
+ context "with a path" do
216
+
217
+ let(:git_path) { tmp_path.join("big-git-repo") }
218
+ let(:sample_path) { git_path.join("buttercup") }
219
+ let(:sample_metadata) do
220
+ Helpers.strip_heredoc(<<-METADATA)
221
+ version "0.6.5"
222
+ METADATA
223
+ end
224
+
225
+ before do
226
+ git_path.rmtree if git_path.exist?
227
+ git_path.mkpath
228
+ sample_path.mkpath
229
+ sample_path.join("metadata.rb").open("wb") { |f| f.write(sample_metadata) }
230
+ Dir.chdir(git_path) do
231
+ `git init`
232
+ `git config user.name "Simba"`
233
+ `git config user.email "simba@savannah-pride.gov"`
234
+ `git add .`
235
+ `git commit -m "Initial commit."`
236
+ end
237
+ end
238
+
239
+ context "if no path option is given" do
240
+ let(:repo_path) { tmp_path.join("repo/resolve") }
241
+ before do
242
+ repo_path.rmtree if repo_path.exist?
243
+ repo_path.mkpath
244
+ repo_path.join("cookbooks").mkpath
245
+ cheffile = Helpers.strip_heredoc(<<-CHEFFILE)
246
+ #!/usr/bin/env ruby
247
+ cookbook "sample",
248
+ :git => #{git_path.to_s.inspect}
249
+ CHEFFILE
250
+ repo_path.join("Cheffile").open("wb") { |f| f.write(cheffile) }
251
+ end
252
+
253
+ it "should not resolve" do
254
+ expect{ Action::Resolve.new(env).run }.to raise_error
255
+ end
256
+ end
257
+
258
+ context "if the path option is wrong" do
259
+ let(:repo_path) { tmp_path.join("repo/resolve") }
260
+ before do
261
+ repo_path.rmtree if repo_path.exist?
262
+ repo_path.mkpath
263
+ repo_path.join("cookbooks").mkpath
264
+ cheffile = Helpers.strip_heredoc(<<-CHEFFILE)
265
+ #!/usr/bin/env ruby
266
+ cookbook "sample",
267
+ :git => #{git_path.to_s.inspect},
268
+ :path => "jelly"
269
+ CHEFFILE
270
+ repo_path.join("Cheffile").open("wb") { |f| f.write(cheffile) }
271
+ end
272
+
273
+ it "should not resolve" do
274
+ expect{ Action::Resolve.new(env).run }.to raise_error
275
+ end
276
+ end
277
+
278
+ context "if the path option is right" do
279
+ let(:repo_path) { tmp_path.join("repo/resolve") }
280
+ before do
281
+ repo_path.rmtree if repo_path.exist?
282
+ repo_path.mkpath
283
+ repo_path.join("cookbooks").mkpath
284
+ cheffile = Helpers.strip_heredoc(<<-CHEFFILE)
285
+ #!/usr/bin/env ruby
286
+ cookbook "sample",
287
+ :git => #{git_path.to_s.inspect},
288
+ :path => "buttercup"
289
+ CHEFFILE
290
+ repo_path.join("Cheffile").open("wb") { |f| f.write(cheffile) }
291
+ end
292
+
293
+ context "the resolve" do
294
+ it "should not raise an exception" do
295
+ expect { Action::Resolve.new(env).run }.to_not raise_error
296
+ end
297
+ end
298
+
299
+ context "the results" do
300
+ before { Action::Resolve.new(env).run }
301
+
302
+ it "should create the lockfile" do
303
+ repo_path.join("Cheffile.lock").should exist
304
+ end
305
+ end
306
+ end
307
+
308
+ end
309
+
310
+ context "missing a metadata" do
311
+ let(:git_path) { tmp_path.join("big-git-repo") }
312
+ let(:repo_path) { tmp_path.join("repo/resolve") }
313
+ before do
314
+ git_path.rmtree if git_path.exist?
315
+ git_path.mkpath
316
+ Dir.chdir(git_path) do
317
+ `git init`
318
+ `git config user.name "Simba"`
319
+ `git config user.email "simba@savannah-pride.gov"`
320
+ `touch not-a-metadata`
321
+ `git add .`
322
+ `git commit -m "Initial commit."`
323
+ end
324
+ repo_path.rmtree if repo_path.exist?
325
+ repo_path.mkpath
326
+ repo_path.join("cookbooks").mkpath
327
+ cheffile = Helpers.strip_heredoc(<<-CHEFFILE)
328
+ cookbook "sample",
329
+ :git => #{git_path.to_s.inspect}
330
+ CHEFFILE
331
+ repo_path.join("Cheffile").open("wb") { |f| f.write(cheffile) }
332
+ end
333
+
334
+ context "the resolve" do
335
+ it "should raise an exception" do
336
+ expect { Action::Resolve.new(env).run }.to raise_error
337
+ end
338
+
339
+ it "should explain the problem" do
340
+ expect { Action::Resolve.new(env).run }.
341
+ to raise_error(Error, /no metadata file found/i)
342
+ end
343
+ end
344
+
345
+ context "the results" do
346
+ before { Action::Resolve.new(env).run rescue nil }
347
+
348
+ it "should not create the lockfile" do
349
+ repo_path.join("Cheffile.lock").should_not exist
350
+ end
351
+
352
+ it "should not create the directory for the cookbook" do
353
+ repo_path.join("cookbooks/sample").should_not exist
354
+ end
355
+ end
356
+ end
357
+
358
+ context "when upstream updates" do
359
+ let(:git_path) { tmp_path.join("upstream-updates-repo") }
360
+ let(:repo_path) { tmp_path.join("repo/resolve-with-upstream-updates") }
361
+
362
+ let(:sample_metadata) do
363
+ Helpers.strip_heredoc(<<-METADATA)
364
+ version "0.6.5"
365
+ METADATA
366
+ end
367
+ before do
368
+
369
+ # set up the git repo as normal, but let's also set up a release-stable branch
370
+ # from which our Cheffile will only pull stable releases
371
+ git_path.rmtree if git_path.exist?
372
+ git_path.mkpath
373
+ git_path.join("metadata.rb").open("w+b"){|f| f.write(sample_metadata)}
374
+
375
+ Dir.chdir(git_path) do
376
+ `git init`
377
+ `git config user.name "Simba"`
378
+ `git config user.email "simba@savannah-pride.gov"`
379
+ `git add metadata.rb`
380
+ `git commit -m "Initial Commit."`
381
+ `git checkout -b some-branch --quiet`
382
+ `echo 'hi' > some-file`
383
+ `git add some-file`
384
+ `git commit -m 'Some File.'`
385
+ `git checkout master --quiet`
386
+ end
387
+
388
+ # set up the chef repo as normal, except the Cheffile points to the release-stable
389
+ # branch - we expect when the upstream copy of that branch is changed, then we can
390
+ # fetch & merge those changes when we update
391
+ repo_path.rmtree if repo_path.exist?
392
+ repo_path.mkpath
393
+ repo_path.join("cookbooks").mkpath
394
+ cheffile = Helpers.strip_heredoc(<<-CHEFFILE)
395
+ cookbook "sample",
396
+ :git => #{git_path.to_s.inspect},
397
+ :ref => "some-branch"
398
+ CHEFFILE
399
+ repo_path.join("Cheffile").open("wb") { |f| f.write(cheffile) }
400
+ Action::Resolve.new(env).run
401
+
402
+ # change the upstream copy of that branch: we expect to be able to pull the latest
403
+ # when we re-resolve
404
+ Dir.chdir(git_path) do
405
+ `git checkout some-branch --quiet`
406
+ `echo 'ho' > some-other-file`
407
+ `git add some-other-file`
408
+ `git commit -m 'Some Other File.'`
409
+ `git checkout master --quiet`
410
+ end
411
+ end
412
+
413
+ let(:metadata_file) { repo_path.join("cookbooks/sample/metadata.rb") }
414
+ let(:old_code_file) { repo_path.join("cookbooks/sample/some-file") }
415
+ let(:new_code_file) { repo_path.join("cookbooks/sample/some-other-file") }
416
+
417
+ context "when updating not a cookbook from that source" do
418
+ before do
419
+ Action::Update.new(env).run
420
+ end
421
+
422
+ it "should pull the tip from upstream" do
423
+ Action::Install.new(env).run
424
+
425
+ metadata_file.should exist #sanity
426
+ old_code_file.should exist #sanity
427
+
428
+ new_code_file.should_not exist # the assertion
429
+ end
430
+ end
431
+
432
+ context "when updating a cookbook from that source" do
433
+ before do
434
+ Action::Update.new(env, :names => %w(sample)).run
435
+ end
436
+
437
+ it "should pull the tip from upstream" do
438
+ Action::Install.new(env).run
439
+
440
+ metadata_file.should exist #sanity
441
+ old_code_file.should exist #sanity
442
+
443
+ new_code_file.should exist # the assertion
444
+ end
445
+ end
446
+ end
447
+
448
+ end
449
+ end
450
+ end
451
+ end
@@ -0,0 +1,217 @@
1
+ require 'pathname'
2
+ require 'json'
3
+ require 'webmock'
4
+
5
+ require 'librarian'
6
+ require 'librarian/helpers'
7
+ require 'librarian/action/resolve'
8
+ require 'librarian/action/install'
9
+ require 'librarian/chef'
10
+
11
+ module Librarian
12
+ module Chef
13
+ module Source
14
+ describe Site do
15
+
16
+ include WebMock::API
17
+
18
+ let(:project_path) do
19
+ project_path = Pathname.new(__FILE__).expand_path
20
+ project_path = project_path.dirname until project_path.join("Rakefile").exist?
21
+ project_path
22
+ end
23
+ let(:tmp_path) { project_path.join("tmp/spec/integration/chef/source/site") }
24
+ after { tmp_path.rmtree if tmp_path && tmp_path.exist? }
25
+ let(:sample_path) { tmp_path.join("sample") }
26
+ let(:sample_metadata) do
27
+ Helpers.strip_heredoc(<<-METADATA)
28
+ version "0.6.5"
29
+ METADATA
30
+ end
31
+
32
+ let(:api_url) { "http://site.cookbooks.com" }
33
+
34
+ let(:sample_index_data) do
35
+ {
36
+ "name" => "sample",
37
+ "versions" => [
38
+ "#{api_url}/cookbooks/sample/versions/0_6_5"
39
+ ]
40
+ }
41
+ end
42
+ let(:sample_0_6_5_data) do
43
+ {
44
+ "version" => "0.6.5",
45
+ "file" => "#{api_url}/cookbooks/sample/versions/0_6_5/file.tar.gz"
46
+ }
47
+ end
48
+ let(:sample_0_6_5_package) do
49
+ s = StringIO.new
50
+ z = Zlib::GzipWriter.new(s, Zlib::NO_COMPRESSION)
51
+ t = Archive::Tar::Minitar::Output.new(z)
52
+ t.tar.add_file_simple("sample/metadata.rb", :mode => 0700,
53
+ :size => sample_metadata.bytesize){|io| io.write(sample_metadata)}
54
+ t.close
55
+ z.close unless z.closed?
56
+ s.string
57
+ end
58
+
59
+ # depends on repo_path being defined in each context
60
+ let(:env) { Environment.new(:project_path => repo_path) }
61
+
62
+ before do
63
+ stub_request(:get, "#{api_url}/cookbooks/sample").
64
+ to_return(:body => JSON.dump(sample_index_data))
65
+
66
+ stub_request(:get, "#{api_url}/cookbooks/sample/versions/0_6_5").
67
+ to_return(:body => JSON.dump(sample_0_6_5_data))
68
+
69
+ stub_request(:get, "#{api_url}/cookbooks/sample/versions/0_6_5/file.tar.gz").
70
+ to_return(:body => sample_0_6_5_package)
71
+ end
72
+
73
+ after do
74
+ WebMock.reset!
75
+ end
76
+
77
+ context "a single dependency with a site source" do
78
+
79
+ context "resolving" do
80
+ let(:repo_path) { tmp_path.join("repo/resolve") }
81
+ before do
82
+ repo_path.rmtree if repo_path.exist?
83
+ repo_path.mkpath
84
+ repo_path.join("cookbooks").mkpath
85
+ cheffile = Helpers.strip_heredoc(<<-CHEFFILE)
86
+ #!/usr/bin/env ruby
87
+ cookbook "sample", :site => #{api_url.inspect}
88
+ CHEFFILE
89
+ repo_path.join("Cheffile").open("wb") { |f| f.write(cheffile) }
90
+ end
91
+
92
+ context "the resolve" do
93
+ it "should not raise an exception" do
94
+ expect { Action::Resolve.new(env).run }.to_not raise_error
95
+ end
96
+ end
97
+
98
+ context "the results" do
99
+ before { Action::Resolve.new(env).run }
100
+
101
+ it "should create the lockfile" do
102
+ repo_path.join("Cheffile.lock").should exist
103
+ end
104
+
105
+ it "should not attempt to install the cookbok" do
106
+ repo_path.join("cookbooks/sample").should_not exist
107
+ end
108
+ end
109
+ end
110
+
111
+ context "intalling" do
112
+ let(:repo_path) { tmp_path.join("repo/install") }
113
+ before do
114
+ repo_path.rmtree if repo_path.exist?
115
+ repo_path.mkpath
116
+ repo_path.join("cookbooks").mkpath
117
+ cheffile = Helpers.strip_heredoc(<<-CHEFFILE)
118
+ #!/usr/bin/env ruby
119
+ cookbook "sample", :site => #{api_url.inspect}
120
+ CHEFFILE
121
+ repo_path.join("Cheffile").open("wb") { |f| f.write(cheffile) }
122
+
123
+ Action::Resolve.new(env).run
124
+ end
125
+
126
+ context "the install" do
127
+ it "should not raise an exception" do
128
+ expect { Action::Install.new(env).run }.to_not raise_error
129
+ end
130
+ end
131
+
132
+ context "the results" do
133
+ before { Action::Install.new(env).run }
134
+
135
+ it "should create the lockfile" do
136
+ repo_path.join("Cheffile.lock").should exist
137
+ end
138
+
139
+ it "should create a directory for the cookbook" do
140
+ repo_path.join("cookbooks/sample").should exist
141
+ end
142
+
143
+ it "should copy the cookbook files into the cookbook directory" do
144
+ repo_path.join("cookbooks/sample/metadata.rb").should exist
145
+ end
146
+ end
147
+ end
148
+
149
+ context "resolving and separately installing" do
150
+ let(:repo_path) { tmp_path.join("repo/resolve-install") }
151
+ before do
152
+ repo_path.rmtree if repo_path.exist?
153
+ repo_path.mkpath
154
+ repo_path.join("cookbooks").mkpath
155
+ cheffile = Helpers.strip_heredoc(<<-CHEFFILE)
156
+ #!/usr/bin/env ruby
157
+ cookbook "sample", :site => #{api_url.inspect}
158
+ CHEFFILE
159
+ repo_path.join("Cheffile").open("wb") { |f| f.write(cheffile) }
160
+
161
+ Action::Resolve.new(env).run
162
+ repo_path.join("tmp").rmtree if repo_path.join("tmp").exist?
163
+ end
164
+
165
+ context "the install" do
166
+ it "should not raise an exception" do
167
+ expect { Action::Install.new(env).run }.to_not raise_error
168
+ end
169
+ end
170
+
171
+ context "the results" do
172
+ before { Action::Install.new(env).run }
173
+
174
+ it "should create a directory for the cookbook" do
175
+ repo_path.join("cookbooks/sample").should exist
176
+ end
177
+
178
+ it "should copy the cookbook files into the cookbook directory" do
179
+ repo_path.join("cookbooks/sample/metadata.rb").should exist
180
+ end
181
+ end
182
+ end
183
+
184
+ end
185
+
186
+ context "when the repo path has a space" do
187
+
188
+ let(:repo_path) { tmp_path.join("repo/with extra spaces/resolve") }
189
+
190
+ before do
191
+ repo_path.rmtree if repo_path.exist?
192
+ repo_path.mkpath
193
+ repo_path.join("cookbooks").mkpath
194
+
195
+ cheffile = Helpers.strip_heredoc(<<-CHEFFILE)
196
+ #!/usr/bin/env ruby
197
+ cookbook "sample", :site => #{api_url.inspect}
198
+ CHEFFILE
199
+ repo_path.join("Cheffile").open("wb") { |f| f.write(cheffile) }
200
+ end
201
+
202
+ after do
203
+ repo_path.rmtree
204
+ end
205
+
206
+ context "the resolution" do
207
+ it "should not raise an exception" do
208
+ expect { Action::Resolve.new(env).run }.to_not raise_error
209
+ end
210
+ end
211
+
212
+ end
213
+
214
+ end
215
+ end
216
+ end
217
+ end