itunes_store_transporter 0.0.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,83 @@
1
+ require "itunes/store/transporter/errors"
2
+
3
+ module ITunes
4
+ module Store
5
+ class Transporter
6
+ class OutputParser
7
+
8
+ ##
9
+ # This class extracts error and warning messages output by +iTMSTransporter+. For each message
10
+ # is creates an instance of ITunes::Store::Transporter::TransporterMessage
11
+ #
12
+
13
+ attr :errors
14
+ attr :warnings
15
+
16
+ ERROR_LINE = />\s+ERROR:\s+(.+)/
17
+ WARNING_LINE = />\s+WARN:\s+(.+)/
18
+
19
+ # Generic messages we want to ignore.
20
+ SKIP_ERRORS = [ /\boperation was not successful/i,
21
+ /\bunable to verify the package/i,
22
+ /\bwill NOT be verified/,
23
+ /^an error has occurred/i,
24
+ /^an error occurred while/i,
25
+ /^unknown operation/i,
26
+ /\bunable to authenticate the package/i ]
27
+
28
+ ##
29
+ # === Arguments
30
+ #
31
+ # [output (Array)] +iTMSTransporter+ output
32
+ #
33
+ def initialize(output)
34
+ @errors = []
35
+ @warnings = []
36
+ parse_output(output) if Array === output
37
+ end
38
+
39
+ private
40
+ def parse_output(output)
41
+ output.each do |line|
42
+ if line =~ ERROR_LINE
43
+ error = $1
44
+ next if SKIP_ERRORS.any? { |skip| error =~ skip }
45
+ errors << create_message(error)
46
+ elsif line =~ WARNING_LINE
47
+ warnings << create_message($1)
48
+ end
49
+ end
50
+
51
+ # Unique messages only. The block form of uniq() not available on Ruby < 1.9.2
52
+ [errors, warnings].each do |e|
53
+ e.replace(e.inject({}) do |uniq, x|
54
+ uniq[x.message] = x
55
+ uniq
56
+ end.values)
57
+ end
58
+ end
59
+
60
+ def create_message(line)
61
+ case line
62
+ when /^(?:ERROR|WARNING)\s+ITMS-(\d+):\s+(.+)/
63
+ code = $1
64
+ message = $2
65
+ when /(.+)\s+\((\d+)\)$/,
66
+ # Is this correct?
67
+ /(.+)\s+errorCode\s+=\s+\((\d+)\)$/
68
+ message = $1
69
+ code = $2
70
+ else
71
+ message = line
72
+ code = nil
73
+ end
74
+
75
+ message.gsub! /^"/, ""
76
+ message.gsub! /"(?: at .+)?$/, ""
77
+
78
+ TransporterMessage.new(message, code ? code.to_i : nil)
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,94 @@
1
+ require "childprocess"
2
+
3
+ module ITunes
4
+ module Store
5
+ class Transporter
6
+
7
+ class Shell # :nodoc:
8
+ attr :path
9
+
10
+ EXE_NAME = "iTMSTransporter"
11
+ WINDOWS_EXE = "#{EXE_NAME}.CMD"
12
+ DEFAULT_UNIX_PATH = "/usr/local/itms/bin/#{EXE_NAME}"
13
+
14
+ class << self
15
+ def windows?
16
+ # We just need to know where iTMSTransporter lives, though cygwin
17
+ # can crow when it receives a Windows path.
18
+ ChildProcess.windows? || ChildProcess.os == :cygwin
19
+ end
20
+
21
+ def default_path
22
+ if windows?
23
+ # The Transporter installer prefers x86
24
+ # But... I think ruby normalizes this to just PROGRAMFILES
25
+ root = ENV["PROGRAMFILES(x86)"] || ENV["PROGRAMFILES"] # Need C:\ in case?
26
+ File.join(root, "itms", WINDOWS_EXE)
27
+ else
28
+ DEFAULT_UNIX_PATH
29
+ end
30
+ end
31
+ end
32
+
33
+ def initialize(path = nil)
34
+ @path = path || self.class.default_path
35
+ end
36
+
37
+ def exec(argv, &block)
38
+ begin
39
+ process = ChildProcess.build(path, *argv)
40
+
41
+ stdout = IO.pipe
42
+ stderr = IO.pipe
43
+
44
+ stdout[1].sync = true
45
+ process.io.stdout = stdout[1]
46
+
47
+ stderr[1].sync = true
48
+ process.io.stderr = stderr[1]
49
+
50
+ process.start
51
+
52
+ stdout[1].close
53
+ stderr[1].close
54
+
55
+ poll(stdout[0], stderr[0], &block)
56
+ rescue ChildProcess::Error, SystemCallError => e
57
+ raise ITunes::Store::Transporter::TransporterError, e.message
58
+ ensure
59
+ process.wait if process.alive?
60
+ [ stdout, stderr ].flatten.each { |io| io.close if !io.closed? }
61
+ end
62
+
63
+ process.exit_code
64
+ end
65
+
66
+ private
67
+ def poll(stdout, stderr)
68
+ read = [ stdout, stderr ]
69
+
70
+ loop do
71
+ if ready = select(read, nil, nil, 1)
72
+ ready.each do |set|
73
+ next unless set.any?
74
+
75
+ set.each do |io|
76
+ if io.eof?
77
+ read.delete(io)
78
+ next
79
+ end
80
+
81
+ name = io == stdout ? :stdout : :stderr
82
+ yield(io.gets, name)
83
+ end
84
+
85
+ end
86
+ end
87
+ break unless read.any?
88
+ end
89
+ end
90
+
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,7 @@
1
+ module ITunes
2
+ module Store
3
+ class Transporter
4
+ VERSION = "0.0.1"
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,511 @@
1
+ require "spec_helper"
2
+ require "stringio"
3
+
4
+ shared_examples_for "a transporter option" do |option, expected|
5
+ it "creates the correct command line argument" do
6
+ ITunes::Store::Transporter::Shell.any_instance.stub(:exec) { |*arg| arg.first.should include(*expected); 0 }
7
+ subject.run(options.merge(option))
8
+ end
9
+ end
10
+
11
+ shared_examples_for "a vendor_id option" do
12
+ it_should_behave_like "a transporter option", { :vendor_id => "vID" }, "-vendor_id", "vID"
13
+ end
14
+
15
+ shared_examples_for "a transporter option that expects a directory" do |option, expected|
16
+ context "when the directory exists" do
17
+ it_should_behave_like "a transporter option", {option => "."}, expected, "."
18
+ end
19
+
20
+ context "when the directory does not exist" do
21
+ it "raises an OptionError" do
22
+ lambda { subject.run(options.merge(option => "__baaaaahd_directory__")) }.should raise_exception(ITunes::Store::Transporter::OptionError, /does not exist/)
23
+ end
24
+ end
25
+ end
26
+
27
+ shared_examples_for "a boolean transporter option" do |option, expected|
28
+ context "when true" do
29
+ it "creates the command line argument" do
30
+ ITunes::Store::Transporter::Shell.any_instance.stub(:exec) { |*arg| arg.first.should include(*expected); 0 }
31
+ subject.run(options.merge(option => true))
32
+ end
33
+ end
34
+
35
+ context "when false" do
36
+ it "does not create the command line argument" do
37
+ ITunes::Store::Transporter::Shell.any_instance.stub(:exec) { |*arg| arg.first.should_not include(*expected); 0 }
38
+ subject.run(options.merge(option => false))
39
+ end
40
+ end
41
+
42
+ context "when not boolean" do
43
+ it "raises an OptionError" do
44
+ lambda { subject.run(options.merge(option => "sshaw")) }.should raise_exception(ITunes::Store::Transporter::OptionError, /does not accept/)
45
+ end
46
+ end
47
+ end
48
+
49
+ shared_examples_for "a required option" do |option|
50
+ it "must have a value" do
51
+ ["", nil].each do |value|
52
+ lambda { subject.run(options.merge(option => value)) }.should raise_exception(ITunes::Store::Transporter::OptionError, /#{option}/)
53
+ end
54
+ end
55
+ end
56
+
57
+ shared_examples_for "a command that accepts a shortname argument" do
58
+ context "when the shortname's invalid" do
59
+ it "raises an OptionError" do
60
+ lambda { subject.run(options.merge(:shortname => "+")) }.should raise_exception(ITunes::Store::Transporter::OptionError, /shortname/)
61
+ end
62
+ end
63
+
64
+ context "when the shortname's valid" do
65
+ it "does not raise an exception" do
66
+ mock_output
67
+ lambda { subject.run(options.merge(:shortname => "Too $hort")) }.should_not raise_exception
68
+ end
69
+ end
70
+ end
71
+
72
+ shared_examples_for "a subclass of Command::Base" do
73
+ it { should be_a_kind_of(ITunes::Store::Transporter::Command::Base) }
74
+
75
+ context "when on Windows" do
76
+ it "automatically sets NoPause to true" do
77
+ ENV["PROGRAMFILES"] = "C:\\"
78
+ shell = ITunes::Store::Transporter::Shell
79
+ shell.any_instance.stub(:exec) { |*arg| arg.first.should include("-WONoPause", "true"); 0 }
80
+ shell.stub(:windows? => true)
81
+ subject.run(options)
82
+ end
83
+ end
84
+
85
+ describe "options" do
86
+ describe ":print_stderr" do
87
+ before :each do
88
+ @realerr = $stderr
89
+ $stderr = StringIO.new
90
+ mock_output(:stderr => ["ERR 1"])
91
+ described_class.new(:print_stderr => print?).run(options)
92
+ end
93
+
94
+ after(:each) { $stderr = @realerr }
95
+
96
+ context "when true" do
97
+ let(:print?) { true }
98
+
99
+ it "prints to stderr" do
100
+ $stderr.string.chomp.should == "ERR 1"
101
+ end
102
+ end
103
+
104
+ context "when false" do
105
+ let(:print?) { false }
106
+
107
+ it "does not print to stderr" do
108
+ $stderr.string.should be_empty
109
+ end
110
+ end
111
+ end
112
+
113
+ # TODO: Needs some DRYing
114
+ describe ":print_stdout" do
115
+ before :each do
116
+ @realout = $stdout
117
+ $stdout = StringIO.new
118
+ mock_output(:stdout => ["OUT 1"])
119
+ described_class.new(:print_stdout => print?).run(options)
120
+ end
121
+
122
+ after(:each) { $stdout = @realout }
123
+
124
+ context "when true" do
125
+ let(:print?) { true }
126
+
127
+ it "prints to stdout" do
128
+ $stdout.string.chomp.should == "OUT 1"
129
+ end
130
+ end
131
+
132
+ context "when false" do
133
+ let(:print?) { false }
134
+
135
+ it "does not print to stdout" do
136
+ $stdout.string.should be_empty
137
+ end
138
+ end
139
+ end
140
+ end
141
+
142
+ context "when successful" do
143
+ it "calls #handle_success" do
144
+ mock_output(:exit => 0)
145
+ subject.should_receive(:handle_success)
146
+ subject.should_not_receive(:handle_error)
147
+ subject.run(options)
148
+ end
149
+ end
150
+
151
+ context "when not successful" do
152
+ it "calls #handler_error" do
153
+ mock_output(:exit => 1)
154
+ subject.should_receive(:handle_error)
155
+ subject.should_not_receive(:handle_success)
156
+ subject.run(options)
157
+ end
158
+
159
+ context "when an error is output to stderr" do
160
+ it "raises an ExecutionError" do
161
+ mock_output(:exit => 1, :stderr => "stderr.errors")
162
+ lambda { subject.run(options) }.should raise_error { |e|
163
+ e.should be_a(ITunes::Store::Transporter::ExecutionError)
164
+
165
+ e.exitstatus.should == 1
166
+ e.errors.should have(2).items
167
+
168
+ # just check one
169
+ e.errors[0].should be_a(ITunes::Store::Transporter::TransporterMessage)
170
+ e.errors[0].code.should == 9000
171
+ e.errors[0].message.should match("Your audio of screwed up!")
172
+ e.errors[1].code.should == 4009
173
+ e.errors[1].message.should match("Chapter timecode is just plain wrong")
174
+ }
175
+ end
176
+ end
177
+ end
178
+ end
179
+
180
+ shared_examples_for "a transporter mode" do
181
+ it_should_behave_like "a subclass of Command::Base"
182
+ it { should be_a_kind_of(ITunes::Store::Transporter::Command::Mode) }
183
+
184
+ it "requires a username" do
185
+ args = options
186
+ args.delete(:username)
187
+ lambda { subject.run(args) }.should raise_error(ITunes::Store::Transporter::OptionError, /username/)
188
+ end
189
+
190
+ it "requires a password" do
191
+ args = options
192
+ args.delete(:password)
193
+ lambda { subject.run(args) }.should raise_error(ITunes::Store::Transporter::OptionError, /password/)
194
+ end
195
+ end
196
+
197
+ shared_examples_for "a command that requires a package argument" do
198
+ it_should_behave_like "a required option", :package
199
+
200
+ context "when a directory" do
201
+ before(:all) do
202
+ @tmpdir = Dir.mktmpdir
203
+ @pkgdir = File.join(@tmpdir, "package.itmsp")
204
+ Dir.mkdir(@pkgdir)
205
+ end
206
+
207
+ after(:all) { FileUtils.rm_rf(@tmpdir) }
208
+
209
+ it "must end in .itmsp" do
210
+ options = create_options(:package => @tmpdir)
211
+ lambda { subject.run(options) }.should raise_error(ITunes::Store::Transporter::OptionError, /must match/i)
212
+
213
+ mock_output(:exit => 0)
214
+ options = create_options(:package => @pkgdir)
215
+ lambda { subject.run(options) }.should_not raise_error
216
+ end
217
+
218
+ it "must exist" do
219
+ options = create_options(:package => File.join(@tmpdir, "badpkg.itmsp"))
220
+ lambda { subject.run(options) }.should raise_error(ITunes::Store::Transporter::OptionError, /does not exist/i)
221
+ end
222
+ end
223
+
224
+ context "when a file" do
225
+ it "raises an OptionError" do
226
+ path = Tempfile.new("").path
227
+ options = create_options(:package => path)
228
+ # TODO: Optout's error message will probably be changed to something more descriptive, change this when that happens
229
+ lambda { subject.run(options) }.should raise_error(ITunes::Store::Transporter::OptionError, /dir/i)
230
+ end
231
+ end
232
+ end
233
+
234
+ describe ITunes::Store::Transporter::Command::Providers do
235
+ it_behaves_like "a transporter mode"
236
+
237
+ subject { described_class.new({}) }
238
+ let(:options) { create_options }
239
+ its(:mode) { should == "provider" }
240
+
241
+ describe "#run" do
242
+ it "returns the shortname and longname for each provider" do
243
+ mock_output(:stdout => "providers.two", :stderr => "stderr.info")
244
+ subject.run(options).should == [ { :longname => "Some Great User", :shortname => "luser" },
245
+ { :longname => "Skye's Taco Eating Service Inc.", :shortname => "conmuchacebolla" } ]
246
+ end
247
+ end
248
+ end
249
+
250
+ describe ITunes::Store::Transporter::Command::Upload do
251
+ it_behaves_like "a transporter mode"
252
+ it_behaves_like "a command that requires a package argument"
253
+ it_behaves_like "a command that accepts a shortname argument"
254
+
255
+ subject { described_class.new({}) }
256
+ let(:options) { create_options(:package => create_package, :transport => "Aspera") }
257
+ after(:each) { FileUtils.rm_rf(options[:package]) }
258
+
259
+ describe "#run" do
260
+ context "when successful" do
261
+ it "returns true" do
262
+ mock_output(:stdout => "stdout.success")
263
+ subject.run(options).should be_true
264
+ end
265
+ end
266
+ end
267
+
268
+ describe "options" do
269
+ describe ":rate" do
270
+ it "must be an integer" do
271
+ lambda { subject.run(options.merge(:rate => "123")) }.should raise_exception(ITunes::Store::Transporter::OptionError, /rate/)
272
+ end
273
+
274
+ it_should_behave_like "a transporter option", {:rate => 123}, "-k", "123"
275
+ end
276
+
277
+ describe ":transport" do
278
+ %w|Aspera Signiant DAV|.each do |name|
279
+ context "with #{name}" do
280
+ it_should_behave_like "a transporter option", {:transport => name}, "-t", name
281
+ end
282
+ end
283
+
284
+ it "is case sensitive" do
285
+ lambda { subject.run(options.merge(:transport => "aspera")) }.should raise_exception(ITunes::Store::Transporter::OptionError)
286
+ end
287
+
288
+ it "raises an OptionError if the transport is not supported" do
289
+ lambda { subject.run(options.merge(:transport => "ftp")) }.should raise_exception(ITunes::Store::Transporter::OptionError)
290
+ end
291
+ end
292
+
293
+ describe ":delete" do
294
+ it_should_behave_like "a boolean transporter option", :delete, "-delete"
295
+ end
296
+
297
+ describe ":log_history" do
298
+ it_should_behave_like "a transporter option that expects a directory", :log_history, "-loghistory"
299
+ end
300
+
301
+ describe ":success" do
302
+ it_should_behave_like "a transporter option that expects a directory", :success, "-success"
303
+ end
304
+
305
+ describe ":failure" do
306
+ it_should_behave_like "a transporter option that expects a directory", :failure, "-failure"
307
+ end
308
+ end
309
+ end
310
+
311
+ describe ITunes::Store::Transporter::Command::Lookup do
312
+ it_behaves_like "a transporter mode"
313
+ it_behaves_like "a command that accepts a shortname argument"
314
+
315
+ subject { described_class.new({}) }
316
+
317
+ let(:options) { create_options(:vendor_id => "X") }
318
+ its(:mode) { should == "lookupMetadata" }
319
+
320
+ # Fake the directory iTMSTransporter creates for the metadata
321
+ before(:each) do
322
+ @tmpdir = Dir.mktmpdir
323
+ Dir.stub(:mktmpdir => @tmpdir)
324
+
325
+ id = options[:vendor_id] || options[:apple_id]
326
+ @package = File.join(@tmpdir, "#{id}.itmsp")
327
+ Dir.mkdir(@package)
328
+
329
+ @metadata = "<x>Metadata</x>"
330
+ File.open(File.join(@package, "metadata.xml"), "w") { |io| io.write(@metadata) }
331
+
332
+ mock_output
333
+ end
334
+
335
+ after(:each) { FileUtils.rm_rf(@tmpdir) }
336
+
337
+ describe "#run" do
338
+ context "when successful" do
339
+ it "returns the metadata and deletes the temp directory used to output the metadata" do
340
+ subject.run(options).should == @metadata
341
+ File.exists?(@tmpdir).should be_false
342
+ end
343
+
344
+ context "when the metadata file was not created" do
345
+ before { FileUtils.rm_rf(@tmpdir) }
346
+
347
+ it "raises a TransporterError" do
348
+ lambda { subject.run(options) }.should raise_exception(ITunes::Store::Transporter::TransporterError, /no metadata file/i)
349
+ end
350
+ end
351
+ end
352
+ end
353
+
354
+ # One of these two should be requied, but they should be mutually exclusive
355
+ describe "options" do
356
+ describe ":vendor_id" do
357
+ let(:options) { create_options({:vendor_id => "vID"}) }
358
+ it_should_behave_like "a vendor_id option"
359
+ end
360
+
361
+ describe ":apple_id" do
362
+ let(:options) { create_options({:apple_id => "aID"}) }
363
+ it_should_behave_like "a transporter option", { :apple_id => "aID" }, "-apple_id", "aID"
364
+ end
365
+ end
366
+ end
367
+
368
+ describe ITunes::Store::Transporter::Command::Schema do
369
+ it_behaves_like "a transporter mode"
370
+ it_behaves_like "a command that accepts a shortname argument"
371
+
372
+ subject { described_class.new({}) }
373
+ let(:options) { create_options(:type => "strict", :version => "film5") }
374
+ its(:mode) { should == "generateSchema" }
375
+
376
+ describe "#run" do
377
+ context "when successful" do
378
+ it "returns the requested schema" do
379
+ mock_output(:stdout => [ "<x>Film Schema</x>" ], :stderr => "stderr.info")
380
+ subject.run(options).should == "<x>Film Schema</x>"
381
+ end
382
+ end
383
+ end
384
+
385
+ describe "options" do
386
+ describe ":version" do
387
+ it_should_behave_like "a transporter option", {:version => "versionX"}, "-schema", "versionX"
388
+ end
389
+
390
+ # Like Upload's :trasport, case sen., limited to a set
391
+ describe ":type" do
392
+ %w|transitional strict|.each do |type|
393
+ context "with #{type}" do
394
+ it_should_behave_like "a transporter option", {:type => type}, "-schemaType", type
395
+ end
396
+ end
397
+ end
398
+ end
399
+ end
400
+
401
+ describe ITunes::Store::Transporter::Command::Status do
402
+ it_behaves_like "a transporter mode"
403
+
404
+ subject { described_class.new({}) }
405
+ let(:options) { create_options(:vendor_id => 123123) }
406
+ its(:mode) { should == "status" }
407
+
408
+ describe "#run" do
409
+ context "when successful" do
410
+ it "returns the status information for the package" do
411
+ mock_output(:stdout => "status.vendor_id_123123", :stderr => "stderr.info")
412
+ subject.run(options).should == {
413
+ :vendor_identifier => "123123",
414
+ :apple_identifier => "123123",
415
+ :itunesconnect_status => "Not ready for sale",
416
+ :upload_created => "2000-01-01 00:00:00",
417
+ :upload_state => "Uploaded",
418
+ :upload_state_id => "1",
419
+ :content_state => "Irie",
420
+ :content_state_id => "2"
421
+ }
422
+ end
423
+ end
424
+ end
425
+
426
+ describe "options" do
427
+ describe ":vendor_id" do
428
+ it_should_behave_like "a vendor_id option"
429
+ end
430
+ end
431
+ end
432
+
433
+ describe ITunes::Store::Transporter::Command::Verify do
434
+ it_behaves_like "a transporter mode"
435
+ it_behaves_like "a command that requires a package argument"
436
+ it_behaves_like "a command that accepts a shortname argument"
437
+
438
+ subject { described_class.new({}) }
439
+ its(:mode) { should == "verify" }
440
+ let(:options) { create_options(:package => create_package) }
441
+
442
+ describe "#run" do
443
+ context "when successful" do #successful means exit(0)
444
+ context "without any errors" do
445
+ it "returns true" do
446
+ mock_output(:stdout => "stdout.success", :stderr => "stderr.info")
447
+ subject.run(options).should be_true
448
+ end
449
+ end
450
+
451
+ # If no packages were verfied it exits with 0 but emits an error message
452
+ context "with errors" do
453
+ it "raises an ExecutionError" do
454
+ mock_output(:exit => 0, :stderr => "stderr.errors");
455
+ lambda { subject.run(options) }.should raise_exception(ITunes::Store::Transporter::ExecutionError)
456
+ end
457
+ end
458
+ end
459
+ end
460
+
461
+ describe "options" do
462
+ describe ":verify_assets" do
463
+ it_should_behave_like "a boolean transporter option", :verify_assets, "-disableAssetVerification"
464
+ end
465
+ end
466
+ end
467
+
468
+ describe ITunes::Store::Transporter::Command::Version do
469
+ subject { described_class.new({}) }
470
+
471
+ def output_version(v)
472
+ ["iTMSTransporter version #{v}\n"]
473
+ end
474
+
475
+ describe "#run" do
476
+ context "when the version is major" do
477
+ it "returns the version" do
478
+ mock_output(:stdout => output_version("1"))
479
+ subject.run.should == "1"
480
+ end
481
+ end
482
+
483
+ context "when the version is major.minor" do
484
+ it "returns the version" do
485
+ mock_output(:stdout => output_version("1.10"))
486
+ subject.run.should == "1.10"
487
+ end
488
+ end
489
+
490
+ context "when the version is major.minor.release" do
491
+ it "returns the version" do
492
+ mock_output(:stdout => output_version("1.10.100"))
493
+ subject.run.should == "1.10.100"
494
+ end
495
+ end
496
+
497
+ context "when the version is major.minor.release.build format" do
498
+ it "returns the version" do
499
+ mock_output(:stdout => output_version("1.10.100.1234"))
500
+ subject.run.should == "1.10.100.1234"
501
+ end
502
+ end
503
+
504
+ context "when the version it's somthing else" do
505
+ it "returns 'Unknown'" do
506
+ mock_output(:stdout => ["bad version here"])
507
+ subject.run.should == "Unknown"
508
+ end
509
+ end
510
+ end
511
+ end